Skip to content

Commit

Permalink
HHH-8051 Gracefully handle not-found to-one associations
Browse files Browse the repository at this point in the history
  • Loading branch information
Naros committed Dec 16, 2021
1 parent 28b8b33 commit b384b37
Show file tree
Hide file tree
Showing 13 changed files with 869 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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]]
Expand Down
20 changes: 20 additions & 0 deletions hibernate-envers/src/main/java/org/hibernate/envers/Audited.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
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.
Expand All @@ -19,6 +21,7 @@
* @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})
Expand All @@ -30,6 +33,23 @@
*/
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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -128,6 +129,7 @@ public Configuration(Properties properties, EnversService enversService, Metadat
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 );
Expand Down Expand Up @@ -226,6 +228,10 @@ public boolean isFindByRevisionExactMatch() {
return findByRevisionExactMatch;
}

public boolean isGlobalLegacyRelationTargetNotFound() {
return globalLegacyRelationTargetNotFound;
}

public boolean isRevisionEndTimestampEnabled() {
return revisionEndTimestampEnabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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;
Expand Down Expand Up @@ -152,6 +153,7 @@ private void addSyntheticIndexProperty(List value, String propertyAccessorName,
final PropertyAuditingData auditingData = new PropertyAuditingData(
indexColumnName,
propertyAccessorName,
RelationTargetNotFoundAction.ERROR,
false,
true,
indexValue
Expand All @@ -170,6 +172,7 @@ private void addMapEnumeratedKey(Value value, String propertyAccessorName, Class
final PropertyAuditingData propertyAuditingData = new PropertyAuditingData(
indexColumnName,
propertyAccessorName,
RelationTargetNotFoundAction.ERROR,
true,
true,
indexValue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand Down Expand Up @@ -63,7 +64,7 @@ public void addToOne(
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,
Expand Down Expand Up @@ -186,4 +187,16 @@ void addOneToOnePrimaryKeyJoinColumn(
)
);
}

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 );
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
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;
Expand Down Expand Up @@ -594,6 +595,7 @@ protected boolean checkAudited(
}
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() ) ) {
Expand Down Expand Up @@ -735,12 +737,42 @@ protected boolean isOverriddenAudited(XClass 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() {
@Override
public RelationTargetAuditMode targetAuditMode() {
return RelationTargetAuditMode.AUDITED;
}

@Override
public RelationTargetNotFoundAction targetNotFoundAction() {
return RelationTargetNotFoundAction.DEFAULT;
}

@Override
public Class[] auditParents() {
return new Class[0];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
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;
Expand All @@ -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;
Expand Down Expand Up @@ -68,6 +70,7 @@ public PropertyAuditingData(
name,
accessType,
RelationTargetAuditMode.AUDITED,
RelationTargetNotFoundAction.DEFAULT,
null,
null,
forceInsertable,
Expand All @@ -81,20 +84,23 @@ public 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
*/
public PropertyAuditingData(
String name,
String accessType,
RelationTargetNotFoundAction relationTargetNotFoundAction,
boolean forceInsertable,
boolean synthetic,
Value value) {
this(
name,
accessType,
RelationTargetAuditMode.AUDITED,
relationTargetNotFoundAction,
null,
null,
forceInsertable,
Expand All @@ -107,6 +113,7 @@ public PropertyAuditingData(
String name,
String accessType,
RelationTargetAuditMode relationTargetAuditMode,
RelationTargetNotFoundAction relationTargetNotFoundAction,
String auditMappedBy,
String positionMappedBy,
boolean forceInsertable,
Expand All @@ -116,6 +123,7 @@ public PropertyAuditingData(
this.beanName = name;
this.accessType = accessType;
this.relationTargetAuditMode = relationTargetAuditMode;
this.relationTargetNotFoundAction = relationTargetNotFoundAction;
this.auditMappedBy = auditMappedBy;
this.positionMappedBy = positionMappedBy;
this.forceInsertable = forceInsertable;
Expand Down Expand Up @@ -285,6 +293,14 @@ public void setRelationTargetAuditMode(RelationTargetAuditMode relationTargetAud
this.relationTargetAuditMode = relationTargetAuditMode;
}

public RelationTargetNotFoundAction getRelationTargetNotFoundAction() {
return relationTargetNotFoundAction;
}

public void setRelationTargetNotFoundAction(RelationTargetNotFoundAction relationTargetNotFoundAction) {
this.relationTargetNotFoundAction = relationTargetNotFoundAction;
}

public boolean isSynthetic() {
return synthetic;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,20 +157,7 @@ public void nullSafeMapToEntityFromMap(
}
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(
Expand Down Expand Up @@ -208,4 +195,25 @@ public void addMiddleEqualToQuery(
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();
}
}

0 comments on commit b384b37

Please sign in to comment.