diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/IdMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/IdMetadataGenerator.java index eaca0f59fff6..0a8407123f68 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/IdMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/IdMetadataGenerator.java @@ -178,6 +178,7 @@ else if ( hasNotAuditedEntityConfiguration( referencedEntityName ) ) { referencedEntityName, prefixedMapper, true, + false, false ); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ToOneRelationMetadataGenerator.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ToOneRelationMetadataGenerator.java index 955a7aa1bc7c..04d6db7d6942 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ToOneRelationMetadataGenerator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/ToOneRelationMetadataGenerator.java @@ -4,6 +4,7 @@ */ package org.hibernate.envers.configuration.internal.metadata; +import org.hibernate.envers.RelationTargetAuditMode; import org.hibernate.envers.boot.EnversMappingException; import org.hibernate.envers.boot.model.AttributeContainer; import org.hibernate.envers.boot.spi.EnversMetadataBuildingContext; @@ -62,7 +63,8 @@ public void addToOne( referencedEntityName, relMapper, insertable, - shouldIgnoreNotFoundRelation( propertyAuditingData, value ) + shouldIgnoreNotFoundRelation( propertyAuditingData, value ), + propertyAuditingData.getRelationTargetAuditMode() == RelationTargetAuditMode.NOT_AUDITED ); // If the property isn't insertable, checking if this is not a "fake" bidirectional many-to-one relationship, @@ -127,7 +129,8 @@ public void addOneToOneNotOwning( owningReferencePropertyName, referencedEntityName, ownedIdMapper, - MappingTools.ignoreNotFound( value ) + MappingTools.ignoreNotFound( value ), + propertyAuditingData.getRelationTargetAuditMode() == RelationTargetAuditMode.NOT_AUDITED ); // Adding mapper for the id @@ -170,7 +173,8 @@ void addOneToOnePrimaryKeyJoinColumn( referencedEntityName, relMapper, insertable, - MappingTools.ignoreNotFound( value ) + MappingTools.ignoreNotFound( value ), + propertyAuditingData.getRelationTargetAuditMode() == RelationTargetAuditMode.NOT_AUDITED ); // Adding mapper for the id diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/EntityConfiguration.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/EntityConfiguration.java index 36c2f7bc8df6..4137d83faae6 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/EntityConfiguration.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/EntityConfiguration.java @@ -54,7 +54,8 @@ public void addToOneRelation( String toEntityName, IdMapper idMapper, boolean insertable, - boolean ignoreNotFound) { + boolean ignoreNotFound, + boolean targetNotAudited) { relations.put( fromPropertyName, RelationDescription.toOne( @@ -66,7 +67,8 @@ public void addToOneRelation( null, null, insertable, - ignoreNotFound + ignoreNotFound, + targetNotAudited ) ); } @@ -76,7 +78,8 @@ public void addToOneNotOwningRelation( String mappedByPropertyName, String toEntityName, IdMapper idMapper, - boolean ignoreNotFound) { + boolean ignoreNotFound, + boolean targetNotAudited) { relations.put( fromPropertyName, RelationDescription.toOne( @@ -88,7 +91,8 @@ public void addToOneNotOwningRelation( null, null, true, - ignoreNotFound + ignoreNotFound, + targetNotAudited ) ); } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/EntityInstantiator.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/EntityInstantiator.java index 3ff3e7ba9199..9de221bd4f68 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/EntityInstantiator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/EntityInstantiator.java @@ -124,7 +124,8 @@ private void replaceNonAuditIdProxies(Map versionsEntity, Number revision) { enversService.getConfig().getRevisionTypePropertyName() ) ), - enversService + enversService, + false ); originalId.put( key, diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/RelationDescription.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/RelationDescription.java index edadb20a0138..04fc92e6c7d5 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/RelationDescription.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/RelationDescription.java @@ -18,6 +18,7 @@ public class RelationDescription { private final String toEntityName; private final String mappedByPropertyName; private final boolean ignoreNotFound; + private final boolean targetNotAudited; private final IdMapper idMapper; private final PropertyMapper fakeBidirectionalRelationMapper; private final PropertyMapper fakeBidirectionalRelationIndexMapper; @@ -37,10 +38,11 @@ public static RelationDescription toOne( PropertyMapper fakeBidirectionalRelationMapper, PropertyMapper fakeBidirectionalRelationIndexMapper, boolean insertable, - boolean ignoreNotFound) { + boolean ignoreNotFound, + boolean targetNotAudited) { return new RelationDescription( fromPropertyName, relationType, toEntityName, mappedByPropertyName, idMapper, - fakeBidirectionalRelationMapper, fakeBidirectionalRelationIndexMapper, null, null, null, insertable, ignoreNotFound, false + fakeBidirectionalRelationMapper, fakeBidirectionalRelationIndexMapper, null, null, null, insertable, ignoreNotFound, targetNotAudited, false ); } @@ -60,10 +62,10 @@ public static RelationDescription toMany( // Envers populates collections by executing dedicated queries. Special handling of // @NotFound(action = NotFoundAction.IGNORE) can be omitted in such case as exceptions // (e.g. EntityNotFoundException, ObjectNotFoundException) are never thrown. - // Therefore assigning false to ignoreNotFound. + // Therefore assigning false to ignoreNotFound and targetNotAudited. return new RelationDescription( fromPropertyName, relationType, toEntityName, mappedByPropertyName, idMapper, fakeBidirectionalRelationMapper, - fakeBidirectionalRelationIndexMapper, referencingIdData, referencedIdData, auditMiddleEntityName, insertable, false, indexed + fakeBidirectionalRelationIndexMapper, referencingIdData, referencedIdData, auditMiddleEntityName, insertable, false, false, indexed ); } @@ -80,12 +82,14 @@ private RelationDescription( String auditMiddleEntityName, boolean insertable, boolean ignoreNotFound, + boolean targetNotAudited, boolean indexed) { this.fromPropertyName = fromPropertyName; this.relationType = relationType; this.toEntityName = toEntityName; this.mappedByPropertyName = mappedByPropertyName; this.ignoreNotFound = ignoreNotFound; + this.targetNotAudited = targetNotAudited; this.idMapper = idMapper; this.fakeBidirectionalRelationMapper = fakeBidirectionalRelationMapper; this.fakeBidirectionalRelationIndexMapper = fakeBidirectionalRelationIndexMapper; @@ -117,6 +121,10 @@ public boolean isIgnoreNotFound() { return ignoreNotFound; } + public boolean isTargetNotAudited() { + return targetNotAudited; + } + public IdMapper getIdMapper() { return idMapper; } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneEntityLoader.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneEntityLoader.java index 3312585d327f..1886e7f8d7dd 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneEntityLoader.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneEntityLoader.java @@ -28,17 +28,19 @@ public static Object loadImmediate( Object entityId, Number revision, boolean removed, - EnversService enversService) { - if ( enversService.getEntitiesConfigurations().getNotVersionEntityConfiguration( entityName ) == null ) { + EnversService enversService, + boolean isTargetNotAudited) { + + if ( isTargetNotAudited || enversService.getEntitiesConfigurations().getNotVersionEntityConfiguration( entityName ) != null ) { + // Not audited relation, look up entity with Hibernate. + return versionsReader.getSessionImplementor().immediateLoad( entityName, entityId ); + } + else { // Audited relation, look up entity with Envers. // When user traverses removed entities graph, do not restrict revision type of referencing objects // to ADD or MOD (DEL possible). See HHH-5845. return versionsReader.find( entityClass, entityName, entityId, revision, removed ); } - else { - // Not audited relation, look up entity with Hibernate. - return versionsReader.getSessionImplementor().immediateLoad( entityName, entityId ); - } } /** @@ -51,14 +53,15 @@ public static Object createProxy( Object entityId, Number revision, boolean removed, - EnversService enversService) { + EnversService enversService, + boolean isTargetNotAudited) { final EntityPersister persister = versionsReader.getSessionImplementor() .getFactory() .getMappingMetamodel() .getEntityDescriptor( entityName ); return persister.createProxy( entityId, - new ToOneDelegateSessionImplementor( versionsReader, entityClass, entityId, revision, removed, enversService ) + new ToOneDelegateSessionImplementor( versionsReader, entityClass, entityId, revision, removed, enversService, isTargetNotAudited ) ); } @@ -73,14 +76,15 @@ public static Object createProxyOrLoadImmediate( Object entityId, Number revision, boolean removed, - EnversService enversService) { + EnversService enversService, + boolean isTargetNotAudited) { final EntityPersister persister = versionsReader.getSessionImplementor() .getFactory() .getMappingMetamodel() .getEntityDescriptor( entityName ); if ( persister.hasProxy() ) { - return createProxy( versionsReader, entityClass, entityName, entityId, revision, removed, enversService ); + return createProxy( versionsReader, entityClass, entityName, entityId, revision, removed, enversService, isTargetNotAudited ); } - return loadImmediate( versionsReader, entityClass, entityName, entityId, revision, removed, enversService ); + return loadImmediate( versionsReader, entityClass, entityName, entityId, revision, removed, enversService, isTargetNotAudited ); } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java index 63fc88456c25..c6560dc411b3 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/ToOneIdMapper.java @@ -150,6 +150,15 @@ public Object nullSafeMapToEntityFromMap( } else { final EntityInfo referencedEntity = getEntityInfo( enversService, referencedEntityName ); + + // Check if the relation is marked as NOT_AUDITED + final String referencingEntityName = enversService.getEntitiesConfigurations() + .getEntityNameForVersionsEntityName( (String) data.get( "$type$" ) ); + final boolean isTargetNotAudited = referencingEntityName != null && + enversService.getEntitiesConfigurations() + .getRelationDescription( referencingEntityName, getPropertyData().getName() ) + .isTargetNotAudited(); + if ( isIgnoreNotFound( enversService, referencedEntity, data, primaryKey ) ) { // Eagerly loading referenced entity to silence potential (in case of proxy) // EntityNotFoundException or ObjectNotFoundException. Assigning null reference. @@ -160,7 +169,8 @@ public Object nullSafeMapToEntityFromMap( entityId, revision, RevisionType.DEL.equals( data.get( enversService.getConfig().getRevisionTypePropertyName() ) ), - enversService + enversService, + isTargetNotAudited ); } else { @@ -171,7 +181,8 @@ public Object nullSafeMapToEntityFromMap( entityId, revision, RevisionType.DEL.equals( data.get( enversService.getConfig().getRevisionTypePropertyName() ) ), - enversService + enversService, + isTargetNotAudited ); } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/lazy/ToOneDelegateSessionImplementor.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/lazy/ToOneDelegateSessionImplementor.java index 4525b9f18400..50ce52860ffd 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/lazy/ToOneDelegateSessionImplementor.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/lazy/ToOneDelegateSessionImplementor.java @@ -23,6 +23,7 @@ public class ToOneDelegateSessionImplementor extends AbstractDelegateSessionImpl private final Number revision; private final boolean removed; private final EnversService enversService; + private final boolean isTargetNotAudited; public ToOneDelegateSessionImplementor( AuditReaderImplementor versionsReader, @@ -30,7 +31,8 @@ public ToOneDelegateSessionImplementor( Object entityId, Number revision, boolean removed, - EnversService enversService) { + EnversService enversService, + boolean isTargetNotAudited) { super( versionsReader.getSessionImplementor() ); this.versionsReader = versionsReader; this.entityClass = entityClass; @@ -38,6 +40,7 @@ public ToOneDelegateSessionImplementor( this.revision = revision; this.removed = removed; this.enversService = enversService; + this.isTargetNotAudited = isTargetNotAudited; } @Override @@ -49,7 +52,8 @@ public Object doImmediateLoad(String entityName) throws HibernateException { entityId, revision, removed, - enversService + enversService, + isTargetNotAudited ); } } diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/NotAuditedRelationTargetNotFoundActionTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/NotAuditedRelationTargetNotFoundActionTest.java new file mode 100644 index 000000000000..e83fe4bc32e9 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/NotAuditedRelationTargetNotFoundActionTest.java @@ -0,0 +1,263 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.envers.test.integration.query; + +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.Audited; +import org.hibernate.envers.RelationTargetAuditMode; +import org.hibernate.envers.RelationTargetNotFoundAction; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * Tests that {@link org.hibernate.envers.RelationTargetNotFoundAction} works correctly when combined with + * {@link RelationTargetAuditMode#NOT_AUDITED}. When the target entity is deleted, the behavior depends on + * the configured action: ERROR throws {@link jakarta.persistence.EntityNotFoundException}, while IGNORE + * returns null. + * + * To allow deletion of Child entities without foreign key constraint violations, the relationship uses + * {@link jakarta.persistence.ConstraintMode#NO_CONSTRAINT} to prevent database foreign key creation. + * + * @author Minjae Seon + */ +@JiraKey(value = "HHH-19861") +public class NotAuditedRelationTargetNotFoundActionTest extends BaseEnversJPAFunctionalTestCase { + + private Long childForErrorId; + private Long childForIgnoreId; + private Long parentWithErrorId; + private Long parentWithIgnoreId; + + @Entity(name = "Child") + @Table(name = "Child") + @Audited + public static class Child { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String name; + + public Child() { + } + + public Child(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "ParentWithError") + @Table(name = "ParentWithError") + @Audited + public static class ParentWithError { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "child_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED, targetNotFoundAction = RelationTargetNotFoundAction.ERROR) + private Child child; + + public ParentWithError() { + } + + public ParentWithError(String content, Child child) { + this.content = content; + this.child = child; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Child getChild() { + return child; + } + + public void setChild(Child child) { + this.child = child; + } + } + + @Entity(name = "ParentWithIgnore") + @Table(name = "ParentWithIgnore") + @Audited + public static class ParentWithIgnore { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "child_id", nullable = false, foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT)) + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED, targetNotFoundAction = RelationTargetNotFoundAction.IGNORE) + private Child child; + + public ParentWithIgnore() { + } + + public ParentWithIgnore(String content, Child child) { + this.content = content; + this.child = child; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Child getChild() { + return child; + } + + public void setChild(Child child) { + this.child = child; + } + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{ ParentWithError.class, ParentWithIgnore.class, Child.class }; + } + + @Test + @Priority(10) + public void initData() { + EntityManager em = getEntityManager(); + + // Revision 1: Create children and parents + em.getTransaction().begin(); + Child childForError = new Child("Child for Error"); + em.persist(childForError); + Child childForIgnore = new Child("Child for Ignore"); + em.persist(childForIgnore); + + ParentWithError parentWithError = new ParentWithError("Initial content with error", childForError); + em.persist(parentWithError); + ParentWithIgnore parentWithIgnore = new ParentWithIgnore("Initial content with ignore", childForIgnore); + em.persist(parentWithIgnore); + em.getTransaction().commit(); + + childForErrorId = childForError.getId(); + childForIgnoreId = childForIgnore.getId(); + parentWithErrorId = parentWithError.getId(); + parentWithIgnoreId = parentWithIgnore.getId(); + } + + @Test + public void testLoadParentWithErrorAfterChildDeleted() { + EntityManager em = getEntityManager(); + + // Delete the child entity that ParentWithError references + em.getTransaction().begin(); + Child childForError = em.find(Child.class, childForErrorId); + em.remove(childForError); + em.getTransaction().commit(); + + // Now try to load parent's audit history with ERROR action + // This should throw EntityNotFoundException + AuditReader auditReader = getAuditReader(); + ParentWithError parentRev1 = auditReader.find(ParentWithError.class, parentWithErrorId, 1); + + assertNotNull("Parent at revision 1 should not be null", parentRev1); + assertEquals("Initial content with error", parentRev1.getContent()); + + // Try to access the child - should throw EntityNotFoundException + try { + Child childRef = parentRev1.getChild(); + // Access a property to trigger lazy loading + childRef.getName(); + fail("Should have thrown EntityNotFoundException"); + } + catch (EntityNotFoundException e) { + // Expected behavior + } + } + + @Test + public void testLoadParentWithIgnoreAfterChildDeleted() { + EntityManager em = getEntityManager(); + + // Delete the child entity that ParentWithIgnore references + em.getTransaction().begin(); + Child childForIgnore = em.find(Child.class, childForIgnoreId); + em.remove(childForIgnore); + em.getTransaction().commit(); + + // Now try to load parent's audit history with IGNORE action + // This should not throw EntityNotFoundException and return null + AuditReader auditReader = getAuditReader(); + ParentWithIgnore parentRev1 = auditReader.find(ParentWithIgnore.class, parentWithIgnoreId, 1); + + assertNotNull("Parent at revision 1 should not be null", parentRev1); + assertEquals("Initial content with ignore", parentRev1.getContent()); + + // Try to access the child - should return null + Child childRef = parentRev1.getChild(); + assertNull("Child reference should be null when child is deleted and IGNORE action is set", childRef); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/RelationTargetNotAuditedTest.java b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/RelationTargetNotAuditedTest.java new file mode 100644 index 000000000000..5e9b8413757a --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/envers/test/integration/query/RelationTargetNotAuditedTest.java @@ -0,0 +1,202 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.envers.test.integration.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.Audited; +import org.hibernate.envers.RelationTargetAuditMode; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +/** + * Tests that {@link RelationTargetAuditMode#NOT_AUDITED} works correctly when loading audit history. + * When a relation is marked with NOT_AUDITED mode, the target entity is loaded from the current + * table rather than from audit tables, so changes to the target entity are visible when querying + * historical revisions. + * + * @author Minjae Seon + */ +@JiraKey(value = "HHH-19861") +public class RelationTargetNotAuditedTest extends BaseEnversJPAFunctionalTestCase { + + private Long childId; + private Long parentId; + + @Entity(name = "Child") + @Table(name = "Child") + @Audited + public static class Child { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String name; + + public Child() { + } + + public Child(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "Parent") + @Table(name = "Parent") + @Audited + public static class Parent { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String content; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "child_id", nullable = false) + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) + private Child child; + + public Parent() { + } + + public Parent(String content, Child child) { + this.content = content; + this.child = child; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Child getChild() { + return child; + } + + public void setChild(Child child) { + this.child = child; + } + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{ Parent.class, Child.class }; + } + + @Test + @Priority(10) + public void initData() { + EntityManager em = getEntityManager(); + + // Revision 1: Create child and parent + em.getTransaction().begin(); + Child child = new Child("Child 1"); + em.persist(child); + Parent parent = new Parent("Initial content", child); + em.persist(parent); + em.getTransaction().commit(); + + childId = child.getId(); + parentId = parent.getId(); + + // Revision 2: Update parent content + em.getTransaction().begin(); + parent = em.find(Parent.class, parentId); + parent.setContent("Updated content"); + em.getTransaction().commit(); + + // Revision 3: Update child name (should not create audit record for parent) + em.getTransaction().begin(); + child = em.find(Child.class, childId); + child.setName("Child 1 Updated"); + em.getTransaction().commit(); + } + + @Test + public void testLoadParentAtRevision1() { + AuditReader auditReader = getAuditReader(); + Parent parentRev1 = auditReader.find(Parent.class, parentId, 1); + + assertNotNull("Parent at revision 1 should not be null", parentRev1); + assertEquals("Initial content", parentRev1.getContent()); + assertNotNull("Child reference should not be null", parentRev1.getChild()); + assertEquals(childId, parentRev1.getChild().getId()); + // Child should be loaded from current table, so it should have the updated name + assertEquals("Child 1 Updated", parentRev1.getChild().getName()); + } + + @Test + public void testLoadParentAtRevision2() { + AuditReader auditReader = getAuditReader(); + Parent parentRev2 = auditReader.find(Parent.class, parentId, 2); + + assertNotNull("Parent at revision 2 should not be null", parentRev2); + assertEquals("Updated content", parentRev2.getContent()); + assertNotNull("Child reference should not be null", parentRev2.getChild()); + assertEquals(childId, parentRev2.getChild().getId()); + // Child should be loaded from current table + assertEquals("Child 1 Updated", parentRev2.getChild().getName()); + } + + @Test + public void testQueryParentRevisions() { + AuditReader auditReader = getAuditReader(); + List revisions = auditReader.getRevisions(Parent.class, parentId); + + // Parent should have 2 revisions (creation and update) + assertEquals("Parent should have 2 revisions", 2, revisions.size()); + } + + @Test + public void testQueryChildRevisions() { + AuditReader auditReader = getAuditReader(); + List revisions = auditReader.getRevisions(Child.class, childId); + + // Child should have 2 revisions (creation and update) + assertEquals("Child should have 2 revisions", 2, revisions.size()); + } +}