diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionMetadata.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionMetadata.java index 6b92607b1f..5f18b6d591 100755 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionMetadata.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/DefaultRevisionMetadata.java @@ -19,6 +19,7 @@ import java.time.LocalDateTime; import java.time.ZoneOffset; import java.util.Optional; +import java.util.Set; import org.hibernate.envers.DefaultRevisionEntity; @@ -32,22 +33,26 @@ * @author Oliver Gierke * @author Philip Huegelmeyer * @author Jens Schauder + * @author Miguel Ángel Ruiz */ public final class DefaultRevisionMetadata implements RevisionMetadata { private final DefaultRevisionEntity entity; private final RevisionType revisionType; + private final Set changedFields; public DefaultRevisionMetadata(DefaultRevisionEntity entity) { - this(entity, RevisionType.UNKNOWN); + this(entity, RevisionType.UNKNOWN, Set.of()); } - public DefaultRevisionMetadata(DefaultRevisionEntity entity, RevisionType revisionType) { + public DefaultRevisionMetadata(DefaultRevisionEntity entity, RevisionType revisionType, Set changedFields) { Assert.notNull(entity, "DefaultRevisionEntity must not be null"); + Assert.notNull(changedFields, "Changed fields set must not be null"); this.entity = entity; this.revisionType = revisionType; + this.changedFields = changedFields; } public Optional getRevisionNumber() { @@ -74,6 +79,10 @@ public RevisionType getRevisionType() { return revisionType; } + public Set getChangedFields() { + return changedFields; + } + @Override public boolean equals(Object o) { @@ -84,12 +93,13 @@ public boolean equals(Object o) { return false; } DefaultRevisionMetadata that = (DefaultRevisionMetadata) o; - return getRevisionNumber().equals(that.getRevisionNumber()) - && getRevisionInstant().equals(that.getRevisionInstant()) && revisionType.equals(that.getRevisionType()); + return getRevisionNumber().equals(that.getRevisionNumber()) && getRevisionInstant().equals( + that.getRevisionInstant()) && revisionType.equals(that.getRevisionType()) && getChangedFields().equals( + that.getChangedFields()); } @Override public String toString() { - return "DefaultRevisionMetadata{" + "entity=" + entity + ", revisionType=" + revisionType + '}'; + return "DefaultRevisionMetadata{" + "entity=" + entity + ", revisionType=" + revisionType + ", changedFields=" + changedFields + '}'; } } diff --git a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java index 30e4ff2ecc..6afe44596d 100755 --- a/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java +++ b/spring-data-envers/src/main/java/org/springframework/data/envers/repository/support/EnversRevisionRepositoryImpl.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; import org.hibernate.Hibernate; import org.hibernate.envers.AuditReader; @@ -33,6 +34,7 @@ import org.hibernate.envers.RevisionType; import org.hibernate.envers.query.AuditEntity; import org.hibernate.envers.query.AuditQuery; +import org.hibernate.envers.query.AuditQueryCreator; import org.hibernate.envers.query.criteria.AuditProperty; import org.hibernate.envers.query.order.AuditOrder; import org.springframework.data.domain.Page; @@ -65,6 +67,7 @@ * @author Donghun Shin * @author Greg Turnquist * @author Aref Behboodi + * @author Miguel Ángel Ruiz */ @Transactional(readOnly = true) public class EnversRevisionRepositoryImpl> @@ -132,7 +135,14 @@ public Optional> findRevision(ID id, N revisionNumber) { @SuppressWarnings("unchecked") public Revisions findRevisions(ID id) { - List resultList = createBaseQuery(id).getResultList(); + return findRevisions(id, Set.of()); + } + + @SuppressWarnings("unchecked") + @Override + public Revisions findRevisions(ID id, Set changedFields) { + + List resultList = createBaseQuery(id, changedFields).getResultList(); List> revisionList = new ArrayList<>(resultList.size()); for (Object[] objects : resultList) { @@ -172,7 +182,14 @@ private List mapPropertySort(Sort sort) { @SuppressWarnings("unchecked") public Page> findRevisions(ID id, Pageable pageable) { - AuditQuery baseQuery = createBaseQuery(id); + return findRevisions(id, Set.of(), pageable); + } + + @SuppressWarnings("unchecked") + @Override + public Page> findRevisions(ID id, Set changedFields, Pageable pageable) { + + AuditQuery baseQuery = createBaseQuery(id, changedFields); List orderMapped = (pageable.getSort() instanceof RevisionSort revisionSort) ? List.of(mapRevisionSort(revisionSort)) @@ -185,7 +202,7 @@ public Page> findRevisions(ID id, Pageable pageable) { .setMaxResults(pageable.getPageSize()) // .getResultList(); - Long count = (Long) createBaseQuery(id) // + Long count = (Long) createBaseQuery(id, changedFields) // .addProjection(AuditEntity.revisionNumber().count()).getSingleResult(); List> revisions = new ArrayList<>(); @@ -198,12 +215,24 @@ public Page> findRevisions(ID id, Pageable pageable) { private AuditQuery createBaseQuery(ID id) { + return createBaseQuery(id, Set.of()); + } + + private AuditQuery createBaseQuery(ID id, Set changedFields) { + + Assert.notNull(changedFields, "Changed fields must not be null!"); + Class type = entityInformation.getJavaType(); AuditReader reader = AuditReaderFactory.get(entityManager); - return reader.createQuery() // - .forRevisionsOfEntity(type, false, true) // - .add(AuditEntity.id().eq(id)); + AuditQueryCreator auditQueryCreator = reader.createQuery(); + + AuditQuery auditQuery = changedFields.isEmpty() ? auditQueryCreator.forRevisionsOfEntity(type, false, true) // + : auditQueryCreator.forRevisionsOfEntityWithChanges(type, true); + + changedFields.forEach(fieldName -> auditQuery.add(AuditEntity.property(fieldName).hasChanged())); + + return auditQuery.add(AuditEntity.id().eq(id)); } @SuppressWarnings("unchecked") @@ -217,13 +246,14 @@ static class QueryResult { private final T entity; private final Object metadata; private final RevisionMetadata.RevisionType revisionType; + private final Set changedFields; QueryResult(Object[] data) { Assert.notNull(data, "Data must not be null"); Assert.isTrue( // - data.length == 3, // - () -> String.format("Data must have length three, but has length %d.", data.length)); + data.length >= 3, // + () -> String.format("Data must have at least length three, but has length %d.", data.length)); Assert.isTrue( // data[2] instanceof RevisionType, // () -> String.format("The third array element must be of type Revision type, but is of type %s", @@ -232,12 +262,23 @@ static class QueryResult { entity = (T) data[0]; metadata = data[1]; revisionType = convertRevisionType((RevisionType) data[2]); + Set changedFieldsTemp = Set.of(); + + if (data.length == 4) { + Assert.isTrue( // + data[3] instanceof Set, // + () -> String.format("The fourth array element must be of type Set, but is of type %s", data[3].getClass())); + + changedFieldsTemp = (Set) data[3]; + } + + changedFields = changedFieldsTemp; } RevisionMetadata createRevisionMetadata() { return metadata instanceof DefaultRevisionEntity defaultRevisionEntity // - ? new DefaultRevisionMetadata(defaultRevisionEntity, revisionType) // + ? new DefaultRevisionMetadata(defaultRevisionEntity, revisionType, changedFields) // : new AnnotationRevisionMetadata<>(Hibernate.unproxy(metadata), RevisionNumber.class, RevisionTimestamp.class, revisionType); } diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java index 1a4ce785ea..9a49ebff51 100755 --- a/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/repository/support/RepositoryIntegrationTests.java @@ -15,6 +15,7 @@ */ package org.springframework.data.envers.repository.support; +import org.assertj.core.util.IterableUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -23,6 +24,8 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; import org.springframework.data.envers.Config; +import org.springframework.data.envers.sample.Continent; +import org.springframework.data.envers.sample.ContinentRepository; import org.springframework.data.envers.sample.Country; import org.springframework.data.envers.sample.CountryRepository; import org.springframework.data.envers.sample.License; @@ -35,9 +38,12 @@ import java.time.Instant; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.history.RevisionMetadata.RevisionType.*; @@ -48,6 +54,7 @@ * @author Oliver Gierke * @author Jens Schauder * @author Niklas Loechte + * @author Miguel Ángel Ruiz */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = Config.class) @@ -57,12 +64,15 @@ class RepositoryIntegrationTests { LicenseRepository licenseRepository; @Autowired CountryRepository countryRepository; + @Autowired + ContinentRepository continentRepository; @BeforeEach void setUp() { licenseRepository.deleteAll(); countryRepository.deleteAll(); + continentRepository.deleteAll(); } @Test @@ -94,6 +104,15 @@ void testLifeCycle() { countryRepository.save(de); + Continent europe = new Continent(); + europe.name = "Asia"; + + continentRepository.save(europe); + + europe.name = "Europe"; + + continentRepository.save(europe); + Optional> revision = licenseRepository.findLastChangeRevision(license.id); assertThat(revision).hasValueSatisfying(it -> { @@ -105,6 +124,17 @@ void testLifeCycle() { assertThat(latestRevision.getRequiredRevisionNumber()).isEqualTo(it.getRequiredRevisionNumber()); assertThat(latestRevision.getEntity()).isEqualTo(it.getEntity()); }); + + Revisions revisionsWithModifiedFlag = continentRepository.findRevisions(europe.id, + Set.of("name")); + + assertThat(revisionsWithModifiedFlag).matches(revisions -> { + Collection> revisionCollection = IterableUtil.toCollection(revisions); + + Set continentNames = revisionCollection.stream().map(Revision::getEntity).map(continent -> continent.name) + .collect(Collectors.toSet()); + return continentNames.size() == revisionCollection.size(); + }); } @Test // #1 diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/sample/Continent.java b/spring-data-envers/src/test/java/org/springframework/data/envers/sample/Continent.java new file mode 100644 index 0000000000..e6832919bb --- /dev/null +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/sample/Continent.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.envers.sample; + +import jakarta.persistence.Entity; +import org.hibernate.envers.Audited; + +import java.time.Instant; + +/** + * Sample domain class with modified flag. + * + * @author Miguel Ángel Ruiz + */ +@Audited(withModifiedFlag = true) +@Entity +public class Continent extends AbstractEntity { + + public Instant timestamp; + + public String name; + + public String toString() { + return "Continent(timestamp=" + this.timestamp + ", name=" + this.name + ")"; + } +} diff --git a/spring-data-envers/src/test/java/org/springframework/data/envers/sample/ContinentRepository.java b/spring-data-envers/src/test/java/org/springframework/data/envers/sample/ContinentRepository.java new file mode 100644 index 0000000000..b03c92ee34 --- /dev/null +++ b/spring-data-envers/src/test/java/org/springframework/data/envers/sample/ContinentRepository.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.envers.sample; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.history.RevisionRepository; + +/** + * Repository for {@link Continent} objects. + * + * @author Miguel Ángel Ruiz + */ +public interface ContinentRepository extends RevisionRepository, JpaRepository { + +}