diff --git a/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc b/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc index 5cb694e8eb..ffa7d0ed0a 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/envers/Envers.adoc @@ -321,6 +321,12 @@ This behavior is great when you want to find the snapshot of a non-related entit The new (optional) behavior when this option is enabled forces the query to perform an exact-match instead. In order for these methods to return a non-`null` value, a revision entry must exist for the entity with the specified primary key and revision number; otherwise the result will be `null`. +`*org.hibernate.envers.global_relation_not_found_legacy_flag*` (default: `true` ):: +Globally defines whether legacy relation not-found behavior should be used or not. ++ +By specifying `true`, any `EntityNotFoundException` errors will be thrown unless the `Audited` annotation explicitly specifies to _ignore_ not-found relations. +By specifying `false`, any `EntityNotFoundException` will be be ignored unless the `Audited` annotation explicitly specifies to _raise the error_ rather than silently ignore not-found relations. + [IMPORTANT] ==== The following configuration options have been added recently and should be regarded as experimental: @@ -332,6 +338,7 @@ The following configuration options have been added recently and should be regar . `org.hibernate.envers.original_id_prop_name` . `org.hibernate.envers.find_by_revision_exact_match` . `org.hibernate.envers.audit_strategy_validity_revend_timestamp_numeric` +. `org.hibernate.envers.global_relation_not_found_legacy_flag` ==== [[envers-additional-mappings]] diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/Audited.java b/hibernate-envers/src/main/java/org/hibernate/envers/Audited.java index a4e50469f4..8a27af66c0 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/Audited.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/Audited.java @@ -11,6 +11,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.hibernate.Incubating; + /** * When applied to a class, indicates that all of its properties should be audited. * When applied to a field, indicates that this field should be audited. @@ -19,6 +21,7 @@ import java.lang.annotation.Target; * @author Tomasz Bech * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) * @author Michal Skowronek (mskowr at o2 dot pl) + * @author Chris Cranford */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @@ -30,6 +33,23 @@ public @interface Audited { */ RelationTargetAuditMode targetAuditMode() default RelationTargetAuditMode.AUDITED; + /** + * Specifies if the entity that is the relation target isn't found, how should the system react. + * + * The default is to use the behavior configured based on the system property: + * {@link org.hibernate.envers.configuration.EnversSettings#GLOBAL_RELATION_NOT_FOUND_LEGACY_FLAG}. + * + * When the configuration property is {@code true}, this is to use the legacy behavior which + * implies that the system should throw the {@code EntityNotFoundException} errors unless + * the user has explicitly specified the value {@link RelationTargetNotFoundAction#IGNORE}. + * + * When the configuration property is {@code false}, this is to use the new behavior which + * implies that the system should ignore the {@code EntityNotFoundException} errors unless + * the user has explicitly specified the value {@link RelationTargetNotFoundAction#ERROR}. + */ + @Incubating + RelationTargetNotFoundAction targetNotFoundAction() default RelationTargetNotFoundAction.DEFAULT; + /** * Specifies the superclasses for which properties should be audited, even if the superclasses are not * annotated with {@link Audited}. Causes all properties of the listed classes to be audited, just as if the diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/RelationTargetNotFoundAction.java b/hibernate-envers/src/main/java/org/hibernate/envers/RelationTargetNotFoundAction.java new file mode 100644 index 0000000000..aa072b5247 --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/RelationTargetNotFoundAction.java @@ -0,0 +1,34 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.envers; + +/** + * Defines the actions on how to handle {@code EntityNotFoundException} cases when a relation + * between two entities (audited or not) cannot be found in the data store. + * + * @author Chris Cranford + * @see org.hibernate.annotations.NotFoundAction + */ +public enum RelationTargetNotFoundAction { + /** + * Specifies that exception handling should be based on the global system property: + * {@link org.hibernate.envers.configuration.EnversSettings#GLOBAL_RELATION_NOT_FOUND_LEGACY_FLAG}. + */ + DEFAULT, + + /** + * Specifies that exceptions should be thrown regardless of the global system property: + * {@link org.hibernate.envers.configuration.EnversSettings#GLOBAL_RELATION_NOT_FOUND_LEGACY_FLAG}. + */ + ERROR, + + /** + * Specifies that exceptions should be ignored regardless of the global system property: + * {@link org.hibernate.envers.configuration.EnversSettings#GLOBAL_RELATION_NOT_FOUND_LEGACY_FLAG}. + */ + IGNORE +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/Configuration.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/Configuration.java index c7c91d837b..9867a0866e 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/Configuration.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/Configuration.java @@ -71,6 +71,7 @@ public class Configuration { private final boolean modifiedFlagsEnabled; private final boolean modifiedFlagsDefined; private final boolean findByRevisionExactMatch; + private final boolean globalLegacyRelationTargetNotFound; private final boolean trackEntitiesChanged; private boolean trackEntitiesOverride; @@ -128,6 +129,7 @@ public class Configuration { modifiedFlagsEnabled = configProps.getBoolean( EnversSettings.GLOBAL_WITH_MODIFIED_FLAG, false ); findByRevisionExactMatch = configProps.getBoolean( EnversSettings.FIND_BY_REVISION_EXACT_MATCH, false ); + globalLegacyRelationTargetNotFound = configProps.getBoolean( EnversSettings.GLOBAL_RELATION_NOT_FOUND_LEGACY_FLAG, true ); auditTablePrefix = configProps.getString( EnversSettings.AUDIT_TABLE_PREFIX, DEFAULT_PREFIX ); auditTableSuffix = configProps.getString( EnversSettings.AUDIT_TABLE_SUFFIX, DEFAULT_SUFFIX ); @@ -226,6 +228,10 @@ public class Configuration { return findByRevisionExactMatch; } + public boolean isGlobalLegacyRelationTargetNotFound() { + return globalLegacyRelationTargetNotFound; + } + public boolean isRevisionEndTimestampEnabled() { return revisionEndTimestampEnabled; } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/EnversSettings.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/EnversSettings.java index 9e08637702..66e69ef9b8 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/EnversSettings.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/EnversSettings.java @@ -172,4 +172,15 @@ public interface EnversSettings { * @since 4.3.0 */ String CASCADE_DELETE_REVISION = "org.hibernate.envers.cascade_delete_revision"; + + /** + * Globally defines whether legacy relation not-found behavior should be used or not. + * Defaults to {@code true}. + * + * By specifying {@code true}, any {@code EntityNotFoundException} will be thrown unless the containing + * class or property explicitly specifies that use case to be ignored. Conversely, when specifying the + * value {@code false}, the inverse applies and requires explicitly specifying the use case as error so + * that the exception is thrown. + */ + String GLOBAL_RELATION_NOT_FOUND_LEGACY_FLAG = "org.hibernate.envers.global_relation_not_found_legacy_flag"; } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/ClassesAuditingData.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/ClassesAuditingData.java index e698ee4f09..05f34aef87 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/ClassesAuditingData.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/ClassesAuditingData.java @@ -13,6 +13,7 @@ import java.util.Locale; import java.util.Map; import org.hibernate.envers.boot.EnversMappingException; +import org.hibernate.envers.RelationTargetNotFoundAction; import org.hibernate.envers.configuration.internal.metadata.reader.AuditedPropertiesHolder; import org.hibernate.envers.configuration.internal.metadata.reader.ClassAuditingData; import org.hibernate.envers.configuration.internal.metadata.reader.ComponentAuditingData; @@ -152,6 +153,7 @@ public class ClassesAuditingData { final PropertyAuditingData auditingData = new PropertyAuditingData( indexColumnName, propertyAccessorName, + RelationTargetNotFoundAction.ERROR, false, true, indexValue @@ -170,6 +172,7 @@ public class ClassesAuditingData { final PropertyAuditingData propertyAuditingData = new PropertyAuditingData( indexColumnName, propertyAccessorName, + RelationTargetNotFoundAction.ERROR, true, true, indexValue 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 349a27d1eb..8ac6392dd8 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 @@ -9,6 +9,7 @@ package org.hibernate.envers.configuration.internal.metadata; import org.hibernate.envers.boot.EnversMappingException; import org.hibernate.envers.boot.model.AttributeContainer; import org.hibernate.envers.boot.spi.EnversMetadataBuildingContext; +import org.hibernate.envers.RelationTargetNotFoundAction; import org.hibernate.envers.configuration.internal.metadata.reader.PropertyAuditingData; import org.hibernate.envers.internal.entities.EntityConfiguration; import org.hibernate.envers.internal.entities.IdMappingData; @@ -63,7 +64,7 @@ public final class ToOneRelationMetadataGenerator extends AbstractMetadataGenera referencedEntityName, relMapper, insertable, - MappingTools.ignoreNotFound( value ) + shouldIgnoreNotFoundRelation( propertyAuditingData, value ) ); // If the property isn't insertable, checking if this is not a "fake" bidirectional many-to-one relationship, @@ -186,4 +187,16 @@ public final class ToOneRelationMetadataGenerator extends AbstractMetadataGenera ) ); } + + private boolean shouldIgnoreNotFoundRelation(PropertyAuditingData propertyAuditingData, Value value) { + final RelationTargetNotFoundAction action = propertyAuditingData.getRelationTargetNotFoundAction(); + if ( getMetadataBuildingContext().getConfiguration().isGlobalLegacyRelationTargetNotFound() ) { + // When legacy is enabled, the user must explicitly specify IGNORE for it to be ignored. + return MappingTools.ignoreNotFound( value ) || RelationTargetNotFoundAction.IGNORE.equals( action ); + } + else { + // When non-legacy is enabled, the situation is ignored when not ERROR + return !RelationTargetNotFoundAction.ERROR.equals( action ); + } + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java index accf97c9df..557033ab9a 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/AuditedPropertiesReader.java @@ -34,6 +34,7 @@ import org.hibernate.envers.AuditOverrides; import org.hibernate.envers.Audited; import org.hibernate.envers.NotAudited; import org.hibernate.envers.RelationTargetAuditMode; +import org.hibernate.envers.RelationTargetNotFoundAction; import org.hibernate.envers.boot.EnversMappingException; import org.hibernate.envers.boot.internal.ModifiedColumnNameResolver; import org.hibernate.envers.boot.spi.EnversMetadataBuildingContext; @@ -594,6 +595,7 @@ public class AuditedPropertiesReader { } if ( aud != null ) { propertyData.setRelationTargetAuditMode( aud.targetAuditMode() ); + propertyData.setRelationTargetNotFoundAction( getRelationNotFoundAction( property, allClassAudited ) ); propertyData.setUsingModifiedFlag( checkUsingModifiedFlag( aud ) ); propertyData.setModifiedFlagName( ModifiedColumnNameResolver.getName( propertyName, modifiedFlagSuffix ) ); if ( !StringTools.isEmpty( aud.modifiedColumnName() ) ) { @@ -735,12 +737,42 @@ public class AuditedPropertiesReader { return overriddenAuditedClasses.contains( clazz ); } + private RelationTargetNotFoundAction getRelationNotFoundAction(XProperty property, Audited classAudited) { + final Audited propertyAudited = property.getAnnotation( Audited.class ); + + // class isn't annotated, check property + if ( classAudited == null ) { + if ( propertyAudited == null ) { + // both class and property are not annotated, use default behavior + return RelationTargetNotFoundAction.DEFAULT; + } + // Property is annotated use its value + return propertyAudited.targetNotFoundAction(); + } + + // if class is annotated, take its value by default + RelationTargetNotFoundAction action = classAudited.targetNotFoundAction(); + if ( propertyAudited != null ) { + // both places have audited, use the property value only if it is not DEFAULT + if ( !propertyAudited.targetNotFoundAction().equals( RelationTargetNotFoundAction.DEFAULT ) ) { + action = propertyAudited.targetNotFoundAction(); + } + } + + return action; + } + private static final Audited DEFAULT_AUDITED = new Audited() { @Override public RelationTargetAuditMode targetAuditMode() { return RelationTargetAuditMode.AUDITED; } + @Override + public RelationTargetNotFoundAction targetNotFoundAction() { + return RelationTargetNotFoundAction.DEFAULT; + } + @Override public Class[] auditParents() { return new Class[0]; diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PropertyAuditingData.java b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PropertyAuditingData.java index 84e77c5514..0b746e0f65 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PropertyAuditingData.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/configuration/internal/metadata/reader/PropertyAuditingData.java @@ -15,6 +15,7 @@ import jakarta.persistence.EnumType; import org.hibernate.envers.AuditOverride; import org.hibernate.envers.AuditOverrides; import org.hibernate.envers.RelationTargetAuditMode; +import org.hibernate.envers.RelationTargetNotFoundAction; import org.hibernate.envers.internal.entities.PropertyData; import org.hibernate.envers.internal.tools.StringTools; import org.hibernate.mapping.Value; @@ -36,6 +37,7 @@ public class PropertyAuditingData { private String accessType; private final List auditJoinTableOverrides = new ArrayList<>( 0 ); private RelationTargetAuditMode relationTargetAuditMode; + private RelationTargetNotFoundAction relationTargetNotFoundAction; private String auditMappedBy; private String relationMappedBy; private String positionMappedBy; @@ -68,6 +70,7 @@ public class PropertyAuditingData { name, accessType, RelationTargetAuditMode.AUDITED, + RelationTargetNotFoundAction.DEFAULT, null, null, forceInsertable, @@ -81,6 +84,7 @@ public class PropertyAuditingData { * * @param name the property name * @param accessType the access type + * @param relationTargetNotFoundAction the relation target not found action * @param forceInsertable whether the property is forced insertable * @param synthetic whether the property is a synthetic, non-logic column-based property * @param value the mapping model's value @@ -88,6 +92,7 @@ public class PropertyAuditingData { public PropertyAuditingData( String name, String accessType, + RelationTargetNotFoundAction relationTargetNotFoundAction, boolean forceInsertable, boolean synthetic, Value value) { @@ -95,6 +100,7 @@ public class PropertyAuditingData { name, accessType, RelationTargetAuditMode.AUDITED, + relationTargetNotFoundAction, null, null, forceInsertable, @@ -107,6 +113,7 @@ public class PropertyAuditingData { String name, String accessType, RelationTargetAuditMode relationTargetAuditMode, + RelationTargetNotFoundAction relationTargetNotFoundAction, String auditMappedBy, String positionMappedBy, boolean forceInsertable, @@ -116,6 +123,7 @@ public class PropertyAuditingData { this.beanName = name; this.accessType = accessType; this.relationTargetAuditMode = relationTargetAuditMode; + this.relationTargetNotFoundAction = relationTargetNotFoundAction; this.auditMappedBy = auditMappedBy; this.positionMappedBy = positionMappedBy; this.forceInsertable = forceInsertable; @@ -285,6 +293,14 @@ public class PropertyAuditingData { this.relationTargetAuditMode = relationTargetAuditMode; } + public RelationTargetNotFoundAction getRelationTargetNotFoundAction() { + return relationTargetNotFoundAction; + } + + public void setRelationTargetNotFoundAction(RelationTargetNotFoundAction relationTargetNotFoundAction) { + this.relationTargetNotFoundAction = relationTargetNotFoundAction; + } + public boolean isSynthetic() { return synthetic; } 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 ce1cb054f9..c35a9bd4a7 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 @@ -157,20 +157,7 @@ public class ToOneIdMapper extends AbstractToOneMapper { } else { final EntityInfo referencedEntity = getEntityInfo( enversService, referencedEntityName ); - boolean ignoreNotFound = false; - if ( !referencedEntity.isAudited() ) { - final String referencingEntityName = enversService.getEntitiesConfigurations().getEntityNameForVersionsEntityName( (String) data.get( "$type$" ) ); - if ( referencingEntityName == null && primaryKey == null ) { - // HHH-11215 - Fix for NPE when Embeddable with ManyToOne inside ElementCollection - // an embeddable in an element-collection - // todo: perhaps the mapper should account for this instead? - ignoreNotFound = true; - } - else { - ignoreNotFound = enversService.getEntitiesConfigurations().getRelationDescription( referencingEntityName, getPropertyData().getName() ).isIgnoreNotFound(); - } - } - if ( ignoreNotFound ) { + if ( isIgnoreNotFound( enversService, referencedEntity, data, primaryKey ) ) { // Eagerly loading referenced entity to silence potential (in case of proxy) // EntityNotFoundException or ObjectNotFoundException. Assigning null reference. value = ToOneEntityLoader.loadImmediate( @@ -208,4 +195,25 @@ public class ToOneIdMapper extends AbstractToOneMapper { String prefix2) { delegate.addIdsEqualToQuery( parameters, prefix1, delegate, prefix2 ); } + + // todo: is referenced entity needed any longer? + private boolean isIgnoreNotFound( + EnversService enversService, + EntityInfo referencedEntity, + Map data, + Object primaryKey) { + final String referencingEntityName = enversService.getEntitiesConfigurations() + .getEntityNameForVersionsEntityName( (String) data.get( "$type$" ) ); + + if ( referencingEntityName == null && primaryKey == null ) { + // HHH-11215 - Fix for NPE when Embeddable with ManyToOne inside ElementCollection + // an embeddable in an element-collection + // todo: perhaps the mapper should account for this instead? + return true; + } + + return enversService.getEntitiesConfigurations() + .getRelationDescription( referencingEntityName, getPropertyData().getName() ) + .isIgnoreNotFound(); + } } diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundConfigTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundConfigTest.java new file mode 100644 index 0000000000..189f0c4413 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundConfigTest.java @@ -0,0 +1,236 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.envers.integration.basic; + +import java.util.Map; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.Audited; +import org.hibernate.envers.RelationTargetAuditMode; +import org.hibernate.envers.configuration.EnversSettings; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Test that when the {@link EnversSettings#GLOBAL_RELATION_NOT_FOUND_LEGACY_FLAG} is {@code false} + * that the ignore behavior is used by default rather than throwing {@code EntityNotFoundException}. + * + * @author Chris Cranford + */ +@TestForIssue(jiraKey = "HHH-8051") +public class RelationTargetNotFoundConfigTest extends BaseEnversJPAFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Foo.class, Bar.class, FooBar.class }; + } + + @Override + protected void addConfigOptions(Map options) { + super.addConfigOptions( options ); + options.put( EnversSettings.GLOBAL_RELATION_NOT_FOUND_LEGACY_FLAG, Boolean.FALSE ); + } + + + @Test + public void testRelationTargetNotFoundAction() { + // Revision 1, initialize the data for test case + doInJPA( this::entityManagerFactory, entityManager -> { + final Bar bar = new Bar( 1 ); + entityManager.persist( bar ); + + final FooBar fooBar1 = new FooBar( 1, "fooBar" ); + entityManager.persist( fooBar1 ); + + final FooBar fooBar2 = new FooBar( 2, "fooBar2" ); + entityManager.persist( fooBar2 ); + + final Foo foo = new Foo( 1, bar, fooBar1, fooBar2 ); + entityManager.persist( foo ); + } ); + + // This test verifies that everything is fine before doing various record manipulation changes. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get(entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + assertNotNull( rev1.getBar() ); + assertNotNull( rev1.getFooBar() ); + assertNotNull( rev1.getFooBar2() ); + } ); + + // Simulate the removal of main data table data by removing FooBar1 (an audited entity) + doInJPA( this::entityManagerFactory, entityManager -> { + // obviously we assume either there isn't a FK between tables or the users do something like this + entityManager.createNativeQuery( "UPDATE Foo Set fooBar_id = NULL WHERE id = 1" ).executeUpdate(); + entityManager.createNativeQuery( "DELETE FROM FooBar WHERE id = 1" ).executeUpdate(); + } ); + + // This shouldn't fail because the audited entity data is cached in the audit table and exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + assertNotNull( rev1.getFooBar() ); + } ); + + // Simulate the removal of envers data via purge process by removing FooBar2 (an audited entity) + doInJPA( this::entityManagerFactory, entityManager -> { + entityManager.createNativeQuery( "DELETE FROM FooBar_AUD WHERE id = 2" ).executeUpdate(); + } ); + + // Test querying history record where the reference audit row no longer exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + // With RelationTargetNotFoundAction.ERROR, this would throw an EntityNotFoundException. + assertNull( rev1.getFooBar2() ); + } ); + + // this simulates the removal of a non-audited entity from the main table + doInJPA( this::entityManagerFactory, entityManager -> { + // obviously we assume either there isn't a FK between tables or the users do something like this + entityManager.createNativeQuery( "UPDATE Foo SET bar_id = NULL WHERE id = 1" ).executeUpdate(); + entityManager.createNativeQuery( "DELETE FROM Bar WHERE id = 1" ).executeUpdate(); + } ); + + // Test querying history record where the reference non-audited row no longer exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + // With RelationTargetNotFoundAction.ERROR, this would throw an EntityNotFoundException + assertNull( rev1.getBar() ); + } ); + } + + @Audited + @Entity(name = "Foo") + public static class Foo { + @Id + private Integer id; + + @ManyToOne + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) + private Bar bar; + + @ManyToOne + private FooBar fooBar; + + @ManyToOne + private FooBar fooBar2; + + Foo() { + // Required by JPA + } + + Foo(Integer id, Bar bar, FooBar fooBar, FooBar fooBar2) { + this.id = id; + this.bar = bar; + this.fooBar = fooBar; + this.fooBar2 = fooBar2; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Bar getBar() { + return bar; + } + + public void setBar(Bar bar) { + this.bar = bar; + } + + public FooBar getFooBar() { + return fooBar; + } + + public void setFooBar(FooBar fooBar) { + this.fooBar = fooBar; + } + + public FooBar getFooBar2() { + return fooBar2; + } + + public void setFooBar2(FooBar fooBar2) { + this.fooBar2 = fooBar2; + } + } + + @Entity(name = "Bar") + public static class Bar { + @Id + private Integer id; + + Bar() { + // Required by JPA + } + + Bar(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @Audited + @Entity(name = "FooBar") + public static class FooBar { + @Id + private Integer id; + private String name; + + FooBar() { + // Required by JPA + } + + FooBar(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundLegacyTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundLegacyTest.java new file mode 100644 index 0000000000..7021196251 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundLegacyTest.java @@ -0,0 +1,241 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.envers.integration.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityNotFoundException; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.Audited; +import org.hibernate.envers.RelationTargetAuditMode; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; + +import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * Test that when using the legacy default behavior, any {@code EntityNotFoundException} will + * continue to be thrown, ala preserving pre 6.0 behavior. + * + * @author Chris Cranford + */ +@TestForIssue(jiraKey = "HHH-8051") +public class RelationTargetNotFoundLegacyTest extends BaseEnversJPAFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Foo.class, Bar.class, FooBar.class }; + } + + @Test + public void testRelationTargetNotFoundAction() { + // Revision 1, initialize the data for test case + doInJPA( this::entityManagerFactory, entityManager -> { + final Bar bar = new Bar( 1 ); + entityManager.persist( bar ); + + final FooBar fooBar1 = new FooBar( 1, "fooBar" ); + entityManager.persist( fooBar1 ); + + final FooBar fooBar2 = new FooBar( 2, "fooBar2" ); + entityManager.persist( fooBar2 ); + + final Foo foo = new Foo( 1, bar, fooBar1, fooBar2 ); + entityManager.persist( foo ); + } ); + + // This test verifies that everything is fine before doing various record manipulation changes. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get(entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + assertNotNull( rev1.getBar() ); + assertNotNull( rev1.getFooBar() ); + assertNotNull( rev1.getFooBar2() ); + } ); + + // Simulate the removal of main data table data by removing FooBar1 (an audited entity) + doInJPA( this::entityManagerFactory, entityManager -> { + // obviously we assume either there isn't a FK between tables or the users do something like this + entityManager.createNativeQuery( "UPDATE Foo Set fooBar_id = NULL WHERE id = 1" ).executeUpdate(); + entityManager.createNativeQuery( "DELETE FROM FooBar WHERE id = 1" ).executeUpdate(); + } ); + + // This shouldn't fail because the audited entity data is cached in the audit table and exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + assertNotNull( rev1.getFooBar() ); + } ); + + // Simulate the removal of envers data via purge process by removing FooBar2 (an audited entity) + doInJPA( this::entityManagerFactory, entityManager -> { + entityManager.createNativeQuery( "DELETE FROM FooBar_AUD WHERE id = 2" ).executeUpdate(); + } ); + + // Test querying history record where the reference audit row no longer exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + try { + // With RelationTargetNotFoundAction.ERROR, this would throw an EntityNotFoundException. + assertNull( rev1.getFooBar2() ); + fail( "This expected an EntityNotFoundException to be thrown" ); + } + catch ( Exception e ) { + assertTyping(EntityNotFoundException.class, e ); + } + } ); + + // this simulates the removal of a non-audited entity from the main table + doInJPA( this::entityManagerFactory, entityManager -> { + // obviously we assume either there isn't a FK between tables or the users do something like this + entityManager.createNativeQuery( "UPDATE Foo SET bar_id = NULL WHERE id = 1" ).executeUpdate(); + entityManager.createNativeQuery( "DELETE FROM Bar WHERE id = 1" ).executeUpdate(); + } ); + + // Test querying history record where the reference non-audited row no longer exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + try { + // With RelationTargetNotFoundAction.ERROR, this would throw an EntityNotFoundException + assertNull( rev1.getBar() ); + fail( "This expected an EntityNotFoundException to be thrown" ); + } + catch ( Exception e ) { + assertTyping( EntityNotFoundException.class, e ); + } + } ); + } + + @Audited + @Entity(name = "Foo") + public static class Foo { + @Id + private Integer id; + + @ManyToOne + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) + private Bar bar; + + @ManyToOne + private FooBar fooBar; + + @ManyToOne + private FooBar fooBar2; + + Foo() { + // Required by JPA + } + + Foo(Integer id, Bar bar, FooBar fooBar, FooBar fooBar2) { + this.id = id; + this.bar = bar; + this.fooBar = fooBar; + this.fooBar2 = fooBar2; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Bar getBar() { + return bar; + } + + public void setBar(Bar bar) { + this.bar = bar; + } + + public FooBar getFooBar() { + return fooBar; + } + + public void setFooBar(FooBar fooBar) { + this.fooBar = fooBar; + } + + public FooBar getFooBar2() { + return fooBar2; + } + + public void setFooBar2(FooBar fooBar2) { + this.fooBar2 = fooBar2; + } + } + + @Entity(name = "Bar") + public static class Bar { + @Id + private Integer id; + + Bar() { + // Required by JPA + } + + Bar(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @Audited + @Entity(name = "FooBar") + public static class FooBar { + @Id + private Integer id; + private String name; + + FooBar() { + // Required by JPA + } + + FooBar(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundTest.java new file mode 100644 index 0000000000..2c9fb8673b --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/basic/RelationTargetNotFoundTest.java @@ -0,0 +1,227 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.envers.integration.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.Audited; +import org.hibernate.envers.RelationTargetAuditMode; +import org.hibernate.envers.RelationTargetNotFoundAction; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +/** + * Test that when using the override behavior for {@link RelationTargetNotFoundAction#IGNORE} that + * when {@code EntityNotFoundException} would be thrown, they're actually ignored. + * + * @author Chris Cranford + */ +@TestForIssue(jiraKey = "HHH-8051") +public class RelationTargetNotFoundTest extends BaseEnversJPAFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Foo.class, Bar.class, FooBar.class }; + } + + @Test + public void testRelationTargetNotFoundAction() { + // Revision 1, initialize the data for test case + doInJPA( this::entityManagerFactory, entityManager -> { + final Bar bar = new Bar( 1 ); + entityManager.persist( bar ); + + final FooBar fooBar1 = new FooBar( 1, "fooBar" ); + entityManager.persist( fooBar1 ); + + final FooBar fooBar2 = new FooBar( 2, "fooBar2" ); + entityManager.persist( fooBar2 ); + + final Foo foo = new Foo( 1, bar, fooBar1, fooBar2 ); + entityManager.persist( foo ); + } ); + + // This test verifies that everything is fine before doing various record manipulation changes. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get(entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + assertNotNull( rev1.getBar() ); + assertNotNull( rev1.getFooBar() ); + assertNotNull( rev1.getFooBar2() ); + } ); + + // Simulate the removal of main data table data by removing FooBar1 (an audited entity) + doInJPA( this::entityManagerFactory, entityManager -> { + // obviously we assume either there isn't a FK between tables or the users do something like this + entityManager.createNativeQuery( "UPDATE Foo Set fooBar_id = NULL WHERE id = 1" ).executeUpdate(); + entityManager.createNativeQuery( "DELETE FROM FooBar WHERE id = 1" ).executeUpdate(); + } ); + + // This shouldn't fail because the audited entity data is cached in the audit table and exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + assertNotNull( rev1.getFooBar() ); + } ); + + // Simulate the removal of envers data via purge process by removing FooBar2 (an audited entity) + doInJPA( this::entityManagerFactory, entityManager -> { + entityManager.createNativeQuery( "DELETE FROM FooBar_AUD WHERE id = 2" ).executeUpdate(); + } ); + + // Test querying history record where the reference audit row no longer exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + // With RelationTargetNotFoundAction.ERROR, this would throw an EntityNotFoundException. + assertNull( rev1.getFooBar2() ); + } ); + + // this simulates the removal of a non-audited entity from the main table + doInJPA( this::entityManagerFactory, entityManager -> { + // obviously we assume either there isn't a FK between tables or the users do something like this + entityManager.createNativeQuery( "UPDATE Foo SET bar_id = NULL WHERE id = 1" ).executeUpdate(); + entityManager.createNativeQuery( "DELETE FROM Bar WHERE id = 1" ).executeUpdate(); + } ); + + // Test querying history record where the reference non-audited row no longer exists. + doInJPA( this::entityManagerFactory, entityManager -> { + final AuditReader auditReader = AuditReaderFactory.get( entityManager ); + final Foo rev1 = auditReader.find( Foo.class, 1, 1 ); + assertNotNull( rev1 ); + // With RelationTargetNotFoundAction.ERROR, this would throw an EntityNotFoundException + assertNull( rev1.getBar() ); + } ); + } + + @Audited(targetNotFoundAction = RelationTargetNotFoundAction.IGNORE) + @Entity(name = "Foo") + public static class Foo { + @Id + private Integer id; + + @ManyToOne + @Audited(targetAuditMode = RelationTargetAuditMode.NOT_AUDITED) + private Bar bar; + + @ManyToOne + private FooBar fooBar; + + @ManyToOne + private FooBar fooBar2; + + Foo() { + // Required by JPA + } + + Foo(Integer id, Bar bar, FooBar fooBar, FooBar fooBar2) { + this.id = id; + this.bar = bar; + this.fooBar = fooBar; + this.fooBar2 = fooBar2; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Bar getBar() { + return bar; + } + + public void setBar(Bar bar) { + this.bar = bar; + } + + public FooBar getFooBar() { + return fooBar; + } + + public void setFooBar(FooBar fooBar) { + this.fooBar = fooBar; + } + + public FooBar getFooBar2() { + return fooBar2; + } + + public void setFooBar2(FooBar fooBar2) { + this.fooBar2 = fooBar2; + } + } + + @Entity(name = "Bar") + public static class Bar { + @Id + private Integer id; + + Bar() { + // Required by JPA + } + + Bar(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + } + + @Audited + @Entity(name = "FooBar") + public static class FooBar { + @Id + private Integer id; + private String name; + + FooBar() { + // Required by JPA + } + + FooBar(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +}