HHH-8051 Gracefully handle not-found to-one associations

This commit is contained in:
Chris Cranford 2020-02-17 16:52:48 -05:00 committed by Chris Cranford
parent 28b8b33b88
commit b384b37f39
13 changed files with 869 additions and 15 deletions

View File

@ -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]]

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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

View File

@ -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 );
}
}
}

View File

@ -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];

View File

@ -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<AuditOverrideData> 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;
}

View File

@ -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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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;
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
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;
}
}
}