HHH-8051 Gracefully handle not-found to-one associations
This commit is contained in:
parent
28b8b33b88
commit
b384b37f39
|
@ -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.
|
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`.
|
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]
|
[IMPORTANT]
|
||||||
====
|
====
|
||||||
The following configuration options have been added recently and should be regarded as experimental:
|
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.original_id_prop_name`
|
||||||
. `org.hibernate.envers.find_by_revision_exact_match`
|
. `org.hibernate.envers.find_by_revision_exact_match`
|
||||||
. `org.hibernate.envers.audit_strategy_validity_revend_timestamp_numeric`
|
. `org.hibernate.envers.audit_strategy_validity_revend_timestamp_numeric`
|
||||||
|
. `org.hibernate.envers.global_relation_not_found_legacy_flag`
|
||||||
====
|
====
|
||||||
|
|
||||||
[[envers-additional-mappings]]
|
[[envers-additional-mappings]]
|
||||||
|
|
|
@ -11,6 +11,8 @@ import java.lang.annotation.Retention;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
import java.lang.annotation.RetentionPolicy;
|
||||||
import java.lang.annotation.Target;
|
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 class, indicates that all of its properties should be audited.
|
||||||
* When applied to a field, indicates that this field 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 Tomasz Bech
|
||||||
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
|
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
|
||||||
* @author Michal Skowronek (mskowr at o2 dot pl)
|
* @author Michal Skowronek (mskowr at o2 dot pl)
|
||||||
|
* @author Chris Cranford
|
||||||
*/
|
*/
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
|
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
|
||||||
|
@ -30,6 +33,23 @@ public @interface Audited {
|
||||||
*/
|
*/
|
||||||
RelationTargetAuditMode targetAuditMode() default RelationTargetAuditMode.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
|
* 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
|
* annotated with {@link Audited}. Causes all properties of the listed classes to be audited, just as if the
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -71,6 +71,7 @@ public class Configuration {
|
||||||
private final boolean modifiedFlagsEnabled;
|
private final boolean modifiedFlagsEnabled;
|
||||||
private final boolean modifiedFlagsDefined;
|
private final boolean modifiedFlagsDefined;
|
||||||
private final boolean findByRevisionExactMatch;
|
private final boolean findByRevisionExactMatch;
|
||||||
|
private final boolean globalLegacyRelationTargetNotFound;
|
||||||
|
|
||||||
private final boolean trackEntitiesChanged;
|
private final boolean trackEntitiesChanged;
|
||||||
private boolean trackEntitiesOverride;
|
private boolean trackEntitiesOverride;
|
||||||
|
@ -128,6 +129,7 @@ public class Configuration {
|
||||||
modifiedFlagsEnabled = configProps.getBoolean( EnversSettings.GLOBAL_WITH_MODIFIED_FLAG, false );
|
modifiedFlagsEnabled = configProps.getBoolean( EnversSettings.GLOBAL_WITH_MODIFIED_FLAG, false );
|
||||||
|
|
||||||
findByRevisionExactMatch = configProps.getBoolean( EnversSettings.FIND_BY_REVISION_EXACT_MATCH, 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 );
|
auditTablePrefix = configProps.getString( EnversSettings.AUDIT_TABLE_PREFIX, DEFAULT_PREFIX );
|
||||||
auditTableSuffix = configProps.getString( EnversSettings.AUDIT_TABLE_SUFFIX, DEFAULT_SUFFIX );
|
auditTableSuffix = configProps.getString( EnversSettings.AUDIT_TABLE_SUFFIX, DEFAULT_SUFFIX );
|
||||||
|
@ -226,6 +228,10 @@ public class Configuration {
|
||||||
return findByRevisionExactMatch;
|
return findByRevisionExactMatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isGlobalLegacyRelationTargetNotFound() {
|
||||||
|
return globalLegacyRelationTargetNotFound;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isRevisionEndTimestampEnabled() {
|
public boolean isRevisionEndTimestampEnabled() {
|
||||||
return revisionEndTimestampEnabled;
|
return revisionEndTimestampEnabled;
|
||||||
}
|
}
|
||||||
|
|
|
@ -172,4 +172,15 @@ public interface EnversSettings {
|
||||||
* @since 4.3.0
|
* @since 4.3.0
|
||||||
*/
|
*/
|
||||||
String CASCADE_DELETE_REVISION = "org.hibernate.envers.cascade_delete_revision";
|
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";
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import org.hibernate.envers.boot.EnversMappingException;
|
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.AuditedPropertiesHolder;
|
||||||
import org.hibernate.envers.configuration.internal.metadata.reader.ClassAuditingData;
|
import org.hibernate.envers.configuration.internal.metadata.reader.ClassAuditingData;
|
||||||
import org.hibernate.envers.configuration.internal.metadata.reader.ComponentAuditingData;
|
import org.hibernate.envers.configuration.internal.metadata.reader.ComponentAuditingData;
|
||||||
|
@ -152,6 +153,7 @@ public class ClassesAuditingData {
|
||||||
final PropertyAuditingData auditingData = new PropertyAuditingData(
|
final PropertyAuditingData auditingData = new PropertyAuditingData(
|
||||||
indexColumnName,
|
indexColumnName,
|
||||||
propertyAccessorName,
|
propertyAccessorName,
|
||||||
|
RelationTargetNotFoundAction.ERROR,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
indexValue
|
indexValue
|
||||||
|
@ -170,6 +172,7 @@ public class ClassesAuditingData {
|
||||||
final PropertyAuditingData propertyAuditingData = new PropertyAuditingData(
|
final PropertyAuditingData propertyAuditingData = new PropertyAuditingData(
|
||||||
indexColumnName,
|
indexColumnName,
|
||||||
propertyAccessorName,
|
propertyAccessorName,
|
||||||
|
RelationTargetNotFoundAction.ERROR,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
indexValue
|
indexValue
|
||||||
|
|
|
@ -9,6 +9,7 @@ package org.hibernate.envers.configuration.internal.metadata;
|
||||||
import org.hibernate.envers.boot.EnversMappingException;
|
import org.hibernate.envers.boot.EnversMappingException;
|
||||||
import org.hibernate.envers.boot.model.AttributeContainer;
|
import org.hibernate.envers.boot.model.AttributeContainer;
|
||||||
import org.hibernate.envers.boot.spi.EnversMetadataBuildingContext;
|
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.configuration.internal.metadata.reader.PropertyAuditingData;
|
||||||
import org.hibernate.envers.internal.entities.EntityConfiguration;
|
import org.hibernate.envers.internal.entities.EntityConfiguration;
|
||||||
import org.hibernate.envers.internal.entities.IdMappingData;
|
import org.hibernate.envers.internal.entities.IdMappingData;
|
||||||
|
@ -63,7 +64,7 @@ public final class ToOneRelationMetadataGenerator extends AbstractMetadataGenera
|
||||||
referencedEntityName,
|
referencedEntityName,
|
||||||
relMapper,
|
relMapper,
|
||||||
insertable,
|
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,
|
// 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 );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.hibernate.envers.AuditOverrides;
|
||||||
import org.hibernate.envers.Audited;
|
import org.hibernate.envers.Audited;
|
||||||
import org.hibernate.envers.NotAudited;
|
import org.hibernate.envers.NotAudited;
|
||||||
import org.hibernate.envers.RelationTargetAuditMode;
|
import org.hibernate.envers.RelationTargetAuditMode;
|
||||||
|
import org.hibernate.envers.RelationTargetNotFoundAction;
|
||||||
import org.hibernate.envers.boot.EnversMappingException;
|
import org.hibernate.envers.boot.EnversMappingException;
|
||||||
import org.hibernate.envers.boot.internal.ModifiedColumnNameResolver;
|
import org.hibernate.envers.boot.internal.ModifiedColumnNameResolver;
|
||||||
import org.hibernate.envers.boot.spi.EnversMetadataBuildingContext;
|
import org.hibernate.envers.boot.spi.EnversMetadataBuildingContext;
|
||||||
|
@ -594,6 +595,7 @@ public class AuditedPropertiesReader {
|
||||||
}
|
}
|
||||||
if ( aud != null ) {
|
if ( aud != null ) {
|
||||||
propertyData.setRelationTargetAuditMode( aud.targetAuditMode() );
|
propertyData.setRelationTargetAuditMode( aud.targetAuditMode() );
|
||||||
|
propertyData.setRelationTargetNotFoundAction( getRelationNotFoundAction( property, allClassAudited ) );
|
||||||
propertyData.setUsingModifiedFlag( checkUsingModifiedFlag( aud ) );
|
propertyData.setUsingModifiedFlag( checkUsingModifiedFlag( aud ) );
|
||||||
propertyData.setModifiedFlagName( ModifiedColumnNameResolver.getName( propertyName, modifiedFlagSuffix ) );
|
propertyData.setModifiedFlagName( ModifiedColumnNameResolver.getName( propertyName, modifiedFlagSuffix ) );
|
||||||
if ( !StringTools.isEmpty( aud.modifiedColumnName() ) ) {
|
if ( !StringTools.isEmpty( aud.modifiedColumnName() ) ) {
|
||||||
|
@ -735,12 +737,42 @@ public class AuditedPropertiesReader {
|
||||||
return overriddenAuditedClasses.contains( clazz );
|
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() {
|
private static final Audited DEFAULT_AUDITED = new Audited() {
|
||||||
@Override
|
@Override
|
||||||
public RelationTargetAuditMode targetAuditMode() {
|
public RelationTargetAuditMode targetAuditMode() {
|
||||||
return RelationTargetAuditMode.AUDITED;
|
return RelationTargetAuditMode.AUDITED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RelationTargetNotFoundAction targetNotFoundAction() {
|
||||||
|
return RelationTargetNotFoundAction.DEFAULT;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class[] auditParents() {
|
public Class[] auditParents() {
|
||||||
return new Class[0];
|
return new Class[0];
|
||||||
|
|
|
@ -15,6 +15,7 @@ import jakarta.persistence.EnumType;
|
||||||
import org.hibernate.envers.AuditOverride;
|
import org.hibernate.envers.AuditOverride;
|
||||||
import org.hibernate.envers.AuditOverrides;
|
import org.hibernate.envers.AuditOverrides;
|
||||||
import org.hibernate.envers.RelationTargetAuditMode;
|
import org.hibernate.envers.RelationTargetAuditMode;
|
||||||
|
import org.hibernate.envers.RelationTargetNotFoundAction;
|
||||||
import org.hibernate.envers.internal.entities.PropertyData;
|
import org.hibernate.envers.internal.entities.PropertyData;
|
||||||
import org.hibernate.envers.internal.tools.StringTools;
|
import org.hibernate.envers.internal.tools.StringTools;
|
||||||
import org.hibernate.mapping.Value;
|
import org.hibernate.mapping.Value;
|
||||||
|
@ -36,6 +37,7 @@ public class PropertyAuditingData {
|
||||||
private String accessType;
|
private String accessType;
|
||||||
private final List<AuditOverrideData> auditJoinTableOverrides = new ArrayList<>( 0 );
|
private final List<AuditOverrideData> auditJoinTableOverrides = new ArrayList<>( 0 );
|
||||||
private RelationTargetAuditMode relationTargetAuditMode;
|
private RelationTargetAuditMode relationTargetAuditMode;
|
||||||
|
private RelationTargetNotFoundAction relationTargetNotFoundAction;
|
||||||
private String auditMappedBy;
|
private String auditMappedBy;
|
||||||
private String relationMappedBy;
|
private String relationMappedBy;
|
||||||
private String positionMappedBy;
|
private String positionMappedBy;
|
||||||
|
@ -68,6 +70,7 @@ public class PropertyAuditingData {
|
||||||
name,
|
name,
|
||||||
accessType,
|
accessType,
|
||||||
RelationTargetAuditMode.AUDITED,
|
RelationTargetAuditMode.AUDITED,
|
||||||
|
RelationTargetNotFoundAction.DEFAULT,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
forceInsertable,
|
forceInsertable,
|
||||||
|
@ -81,6 +84,7 @@ public class PropertyAuditingData {
|
||||||
*
|
*
|
||||||
* @param name the property name
|
* @param name the property name
|
||||||
* @param accessType the access type
|
* @param accessType the access type
|
||||||
|
* @param relationTargetNotFoundAction the relation target not found action
|
||||||
* @param forceInsertable whether the property is forced insertable
|
* @param forceInsertable whether the property is forced insertable
|
||||||
* @param synthetic whether the property is a synthetic, non-logic column-based property
|
* @param synthetic whether the property is a synthetic, non-logic column-based property
|
||||||
* @param value the mapping model's value
|
* @param value the mapping model's value
|
||||||
|
@ -88,6 +92,7 @@ public class PropertyAuditingData {
|
||||||
public PropertyAuditingData(
|
public PropertyAuditingData(
|
||||||
String name,
|
String name,
|
||||||
String accessType,
|
String accessType,
|
||||||
|
RelationTargetNotFoundAction relationTargetNotFoundAction,
|
||||||
boolean forceInsertable,
|
boolean forceInsertable,
|
||||||
boolean synthetic,
|
boolean synthetic,
|
||||||
Value value) {
|
Value value) {
|
||||||
|
@ -95,6 +100,7 @@ public class PropertyAuditingData {
|
||||||
name,
|
name,
|
||||||
accessType,
|
accessType,
|
||||||
RelationTargetAuditMode.AUDITED,
|
RelationTargetAuditMode.AUDITED,
|
||||||
|
relationTargetNotFoundAction,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
forceInsertable,
|
forceInsertable,
|
||||||
|
@ -107,6 +113,7 @@ public class PropertyAuditingData {
|
||||||
String name,
|
String name,
|
||||||
String accessType,
|
String accessType,
|
||||||
RelationTargetAuditMode relationTargetAuditMode,
|
RelationTargetAuditMode relationTargetAuditMode,
|
||||||
|
RelationTargetNotFoundAction relationTargetNotFoundAction,
|
||||||
String auditMappedBy,
|
String auditMappedBy,
|
||||||
String positionMappedBy,
|
String positionMappedBy,
|
||||||
boolean forceInsertable,
|
boolean forceInsertable,
|
||||||
|
@ -116,6 +123,7 @@ public class PropertyAuditingData {
|
||||||
this.beanName = name;
|
this.beanName = name;
|
||||||
this.accessType = accessType;
|
this.accessType = accessType;
|
||||||
this.relationTargetAuditMode = relationTargetAuditMode;
|
this.relationTargetAuditMode = relationTargetAuditMode;
|
||||||
|
this.relationTargetNotFoundAction = relationTargetNotFoundAction;
|
||||||
this.auditMappedBy = auditMappedBy;
|
this.auditMappedBy = auditMappedBy;
|
||||||
this.positionMappedBy = positionMappedBy;
|
this.positionMappedBy = positionMappedBy;
|
||||||
this.forceInsertable = forceInsertable;
|
this.forceInsertable = forceInsertable;
|
||||||
|
@ -285,6 +293,14 @@ public class PropertyAuditingData {
|
||||||
this.relationTargetAuditMode = relationTargetAuditMode;
|
this.relationTargetAuditMode = relationTargetAuditMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RelationTargetNotFoundAction getRelationTargetNotFoundAction() {
|
||||||
|
return relationTargetNotFoundAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRelationTargetNotFoundAction(RelationTargetNotFoundAction relationTargetNotFoundAction) {
|
||||||
|
this.relationTargetNotFoundAction = relationTargetNotFoundAction;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSynthetic() {
|
public boolean isSynthetic() {
|
||||||
return synthetic;
|
return synthetic;
|
||||||
}
|
}
|
||||||
|
|
|
@ -157,20 +157,7 @@ public class ToOneIdMapper extends AbstractToOneMapper {
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
final EntityInfo referencedEntity = getEntityInfo( enversService, referencedEntityName );
|
final EntityInfo referencedEntity = getEntityInfo( enversService, referencedEntityName );
|
||||||
boolean ignoreNotFound = false;
|
if ( isIgnoreNotFound( enversService, referencedEntity, data, primaryKey ) ) {
|
||||||
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 ) {
|
|
||||||
// Eagerly loading referenced entity to silence potential (in case of proxy)
|
// Eagerly loading referenced entity to silence potential (in case of proxy)
|
||||||
// EntityNotFoundException or ObjectNotFoundException. Assigning null reference.
|
// EntityNotFoundException or ObjectNotFoundException. Assigning null reference.
|
||||||
value = ToOneEntityLoader.loadImmediate(
|
value = ToOneEntityLoader.loadImmediate(
|
||||||
|
@ -208,4 +195,25 @@ public class ToOneIdMapper extends AbstractToOneMapper {
|
||||||
String prefix2) {
|
String prefix2) {
|
||||||
delegate.addIdsEqualToQuery( parameters, prefix1, delegate, 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue