HHH-9062 Allow validity audit strategy to store revision end timestamps on joined subclass audit tables.

This commit is contained in:
Chris Cranford 2021-11-27 04:41:52 -05:00
parent 9124fd84b4
commit dbecdc41ac
9 changed files with 722 additions and 138 deletions

View File

@ -261,6 +261,13 @@ Only used if the `ValidityAuditStrategy` is used, and `org.hibernate.envers.audi
Boolean flag that controls whether the revision end timestamp field is treated as a `Long` data type. Boolean flag that controls whether the revision end timestamp field is treated as a `Long` data type.
Only used if the `ValidityAuditStrategy` is used, and `org.hibernate.envers.audit_strategy_validity_store_revend_timestamp` evaluates to true. Only used if the `ValidityAuditStrategy` is used, and `org.hibernate.envers.audit_strategy_validity_store_revend_timestamp` evaluates to true.
`*org.hibernate.envers.audit_strategy_validity_revend_timestamp_legacy_placement*`(default: `true` )::
Boolean flag that controls whether the revision end timestamp field is propagated to the joined subclass audit tables.
Only used if the `ValidityAuditStrategy` is used, and `org.hibernate.envers.audit_strategy_validity_store_revend_timestamp` evaluates to true.
+
When set to `true`, the legacy mapping behavior is used such that the revision end timestamp is only maintained in the root entity audit table.
When set to `false`, the revision end timestamp is maintained in both the root entity and joined subclass audit tables; allowing the potential to apply database partitioning to the joined subclass tables just like the root entity audit tables.
`*org.hibernate.envers.use_revision_entity_with_native_id*` (default: `true` ):: `*org.hibernate.envers.use_revision_entity_with_native_id*` (default: `true` )::
Boolean flag that determines the strategy of revision number generation. Boolean flag that determines the strategy of revision number generation.
Default implementation of revision entity uses native identifier generator. Default implementation of revision entity uses native identifier generator.

View File

@ -88,6 +88,7 @@ public class Configuration {
private final String embeddableSetOrdinalPropertyName; private final String embeddableSetOrdinalPropertyName;
private final boolean revisionEndTimestampEnabled; private final boolean revisionEndTimestampEnabled;
private final boolean revisionEndTimestampNumeric; private final boolean revisionEndTimestampNumeric;
private final boolean revisionEndTimestampUseLegacyPlacement;
private final Map<String, String> customAuditTableNames = new HashMap<>(); private final Map<String, String> customAuditTableNames = new HashMap<>();
@ -156,10 +157,15 @@ public class Configuration {
EnversSettings.AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_NUMERIC, EnversSettings.AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_NUMERIC,
false false
); );
revisionEndTimestampUseLegacyPlacement = configProps.getBoolean(
EnversSettings.AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_LEGACY_PLACEMENT,
true
);
} }
else { else {
revisionEndTimestampFieldName = null; revisionEndTimestampFieldName = null;
revisionEndTimestampNumeric = false; revisionEndTimestampNumeric = false;
revisionEndTimestampUseLegacyPlacement = true;
} }
embeddableSetOrdinalPropertyName = configProps.getString( embeddableSetOrdinalPropertyName = configProps.getString(
@ -228,6 +234,10 @@ public class Configuration {
return revisionEndTimestampNumeric; return revisionEndTimestampNumeric;
} }
public boolean isRevisionEndTimestampUseLegacyPlacement() {
return revisionEndTimestampUseLegacyPlacement;
}
public String getDefaultCatalogName() { public String getDefaultCatalogName() {
return defaultCatalogName; return defaultCatalogName;
} }

View File

@ -118,6 +118,16 @@ public interface EnversSettings {
*/ */
String AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_NUMERIC = "org.hibernate.envers.audit_strategy_validity_revend_timestamp_numeric"; String AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_NUMERIC = "org.hibernate.envers.audit_strategy_validity_revend_timestamp_numeric";
/**
* Whether to use legacy validity audit strategy revision end timestamp behavior where the field is not
* included as part of the joined entity inheritance subclass audit tables.
*
* Defaults to {@code true}.
*
* @since 6.0
*/
String AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_LEGACY_PLACEMENT = "org.hibernate.envers.audit_strategy_validity_revend_timestamp_legacy_placement";
/** /**
* Name of column used for storing ordinal of the change in sets of embeddable elements. Defaults to {@literal SETORDINAL}. * Name of column used for storing ordinal of the change in sets of embeddable elements. Defaults to {@literal SETORDINAL}.
*/ */

View File

@ -14,6 +14,7 @@ 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.model.BasicAttribute; import org.hibernate.envers.boot.model.BasicAttribute;
import org.hibernate.envers.boot.model.Identifier; import org.hibernate.envers.boot.model.Identifier;
import org.hibernate.envers.boot.model.JoinedSubclassPersistentEntity;
import org.hibernate.envers.boot.model.PersistentEntity; import org.hibernate.envers.boot.model.PersistentEntity;
import org.hibernate.envers.boot.spi.EnversMetadataBuildingContext; import org.hibernate.envers.boot.spi.EnversMetadataBuildingContext;
import org.hibernate.envers.configuration.Configuration; import org.hibernate.envers.configuration.Configuration;
@ -110,11 +111,27 @@ public abstract class AbstractMetadataGenerator {
entity, entity,
metadataBuildingContext.getConfiguration(), metadataBuildingContext.getConfiguration(),
metadataBuildingContext.getConfiguration().getRevisionTypePropertyType(), metadataBuildingContext.getConfiguration().getRevisionTypePropertyType(),
metadataBuildingContext.getConfiguration().getRevisionInfo().getRevisionInfoClass().getName() metadataBuildingContext.getConfiguration().getRevisionInfo().getRevisionInfoClass().getName(),
false
) )
); );
} }
protected void addAuditStrategyRevisionEndTimestampOnly(PersistentEntity entity) {
if ( ( entity instanceof JoinedSubclassPersistentEntity ) ) {
// Only joined subclass entities are allowed to add revision timestamp to associated tables
metadataBuildingContext.getConfiguration().getAuditStrategy().addAdditionalColumns(
new MappingContext(
entity,
metadataBuildingContext.getConfiguration(),
metadataBuildingContext.getConfiguration().getRevisionTypePropertyType(),
metadataBuildingContext.getConfiguration().getRevisionInfo().getRevisionInfoClass().getName(),
true
)
);
}
}
protected void addRevisionTypeToAttributeContainer(AttributeContainer container, boolean key) { protected void addRevisionTypeToAttributeContainer(AttributeContainer container, boolean key) {
container.addAttribute( container.addAttribute(
new BasicAttribute( new BasicAttribute(

View File

@ -342,6 +342,10 @@ public final class AuditMetadataGenerator extends AbstractMetadataGenerator {
// HHH-7940 - New synthetic property support for @IndexColumn/@OrderColumn dynamic properties // HHH-7940 - New synthetic property support for @IndexColumn/@OrderColumn dynamic properties
addSynthetics( entity, auditingData, propertyMapper, mappingData, persistentClass.getEntityName() ); addSynthetics( entity, auditingData, propertyMapper, mappingData, persistentClass.getEntityName() );
if ( !configuration.isRevisionEndTimestampUseLegacyPlacement() ) {
addAuditStrategyRevisionEndTimestampOnly( entity );
}
mappingData.addMapping( entity ); mappingData.addMapping( entity );
// Storing the generated configuration // Storing the generated configuration

View File

@ -9,17 +9,19 @@ package org.hibernate.envers.strategy.internal;
import static org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants.MIDDLE_ENTITY_ALIAS; import static org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants.MIDDLE_ENTITY_ALIAS;
import static org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants.REVISION_PARAMETER; import static org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants.REVISION_PARAMETER;
import java.sql.Connection;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.SQLException; import java.sql.SQLException;
import java.sql.Types; import java.sql.Types;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import org.hibernate.LockOptions; import org.hibernate.LockOptions;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.action.spi.BeforeTransactionCompletionProcess; import org.hibernate.engine.jdbc.spi.JdbcCoordinator;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.envers.RevisionType; import org.hibernate.envers.RevisionType;
@ -28,9 +30,11 @@ import org.hibernate.envers.boot.model.Column;
import org.hibernate.envers.boot.model.ManyToOneAttribute; import org.hibernate.envers.boot.model.ManyToOneAttribute;
import org.hibernate.envers.configuration.Configuration; import org.hibernate.envers.configuration.Configuration;
import org.hibernate.envers.configuration.internal.metadata.RevisionInfoHelper; import org.hibernate.envers.configuration.internal.metadata.RevisionInfoHelper;
import org.hibernate.envers.exception.AuditException;
import org.hibernate.envers.internal.entities.mapper.PersistentCollectionChangeData; import org.hibernate.envers.internal.entities.mapper.PersistentCollectionChangeData;
import org.hibernate.envers.internal.entities.mapper.relation.MiddleComponentData; import org.hibernate.envers.internal.entities.mapper.relation.MiddleComponentData;
import org.hibernate.envers.internal.entities.mapper.relation.MiddleIdData; import org.hibernate.envers.internal.entities.mapper.relation.MiddleIdData;
import org.hibernate.envers.internal.revisioninfo.RevisionInfoNumberReader;
import org.hibernate.envers.internal.synchronization.SessionCacheCleaner; import org.hibernate.envers.internal.synchronization.SessionCacheCleaner;
import org.hibernate.envers.internal.tools.query.Parameters; import org.hibernate.envers.internal.tools.query.Parameters;
import org.hibernate.envers.internal.tools.query.QueryBuilder; import org.hibernate.envers.internal.tools.query.QueryBuilder;
@ -38,7 +42,7 @@ import org.hibernate.envers.strategy.AuditStrategy;
import org.hibernate.envers.strategy.spi.AuditStrategyContext; import org.hibernate.envers.strategy.spi.AuditStrategyContext;
import org.hibernate.envers.strategy.spi.MappingContext; import org.hibernate.envers.strategy.spi.MappingContext;
import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.EventSource;
import org.hibernate.jdbc.ReturningWork; import org.hibernate.persister.entity.JoinedSubclassEntityPersister;
import org.hibernate.persister.entity.Queryable; import org.hibernate.persister.entity.Queryable;
import org.hibernate.persister.entity.UnionSubclassEntityPersister; import org.hibernate.persister.entity.UnionSubclassEntityPersister;
import org.hibernate.property.access.spi.Getter; import org.hibernate.property.access.spi.Getter;
@ -91,6 +95,8 @@ public class ValidityAuditStrategy implements AuditStrategy {
@Override @Override
public void addAdditionalColumns(MappingContext mappingContext) { public void addAdditionalColumns(MappingContext mappingContext) {
if ( !mappingContext.isRevisionEndTimestampOnly() ) {
// Add revision end field since mapping is not requesting only the timestamp.
final ManyToOneAttribute revEndMapping = new ManyToOneAttribute( final ManyToOneAttribute revEndMapping = new ManyToOneAttribute(
mappingContext.getConfiguration().getRevisionEndFieldName(), mappingContext.getConfiguration().getRevisionEndFieldName(),
mappingContext.getRevisionInfoPropertyType(), mappingContext.getRevisionInfoPropertyType(),
@ -106,6 +112,7 @@ public class ValidityAuditStrategy implements AuditStrategy {
); );
mappingContext.getEntityMapping().addAttribute( revEndMapping ); mappingContext.getEntityMapping().addAttribute( revEndMapping );
}
if ( mappingContext.getConfiguration().isRevisionEndTimestampEnabled() ) { if ( mappingContext.getConfiguration().isRevisionEndTimestampEnabled() ) {
// add a column for the timestamp of the end revision // add a column for the timestamp of the end revision
@ -116,16 +123,27 @@ public class ValidityAuditStrategy implements AuditStrategy {
else { else {
revisionInfoTimestampTypeName = StandardBasicTypes.TIMESTAMP.getName(); revisionInfoTimestampTypeName = StandardBasicTypes.TIMESTAMP.getName();
} }
String revEndTimestampPropertyName = mappingContext.getConfiguration().getRevisionEndTimestampFieldName();
String revEndTimestampColumnName = revEndTimestampPropertyName;
if ( !mappingContext.getConfiguration().isRevisionEndTimestampUseLegacyPlacement() ) {
if ( mappingContext.isRevisionEndTimestampOnly() ) {
// properties across a joined inheritance model cannot have the same name.
// what is done here is we adjust just the property name so it is seen as unique in
// the mapping model but keep the column representation with the configured timestamp column name.
revEndTimestampPropertyName = mappingContext.getConfiguration().getRevisionEndTimestampFieldName()
+ "_"
+ mappingContext.getEntityMapping().getAuditTableData().getAuditTableName();
}
}
final BasicAttribute revEndTimestampMapping = new BasicAttribute( final BasicAttribute revEndTimestampMapping = new BasicAttribute(
mappingContext.getConfiguration().getRevisionEndTimestampFieldName(), revEndTimestampPropertyName,
revisionInfoTimestampTypeName, revisionInfoTimestampTypeName,
true, true,
true, true,
false false
); );
revEndTimestampMapping.addColumn( revEndTimestampMapping.addColumn( new Column( revEndTimestampColumnName ) );
new Column( mappingContext.getConfiguration().getRevisionEndTimestampFieldName() )
);
mappingContext.getEntityMapping().addAttribute( revEndTimestampMapping ); mappingContext.getEntityMapping().addAttribute( revEndTimestampMapping );
} }
} }
@ -139,7 +157,6 @@ public class ValidityAuditStrategy implements AuditStrategy {
final Object data, final Object data,
final Object revision) { final Object revision) {
final String auditedEntityName = configuration.getAuditEntityName( entityName ); final String auditedEntityName = configuration.getAuditEntityName( entityName );
final String revisionInfoEntityName = configuration.getRevisionInfo().getRevisionInfoEntityName();
// Save the audit data // Save the audit data
session.save( auditedEntityName, data ); session.save( auditedEntityName, data );
@ -154,122 +171,43 @@ public class ValidityAuditStrategy implements AuditStrategy {
final boolean reuseEntityIdentifier = configuration.isAllowIdentifierReuse(); final boolean reuseEntityIdentifier = configuration.isAllowIdentifierReuse();
if ( reuseEntityIdentifier || getRevisionType( configuration, data ) != RevisionType.ADD ) { if ( reuseEntityIdentifier || getRevisionType( configuration, data ) != RevisionType.ADD ) {
// Register transaction completion process to guarantee execution of UPDATE statement after INSERT. // Register transaction completion process to guarantee execution of UPDATE statement after INSERT.
( (EventSource) session ).getActionQueue().registerProcess( new BeforeTransactionCompletionProcess() { ( (EventSource) session ).getActionQueue().registerProcess( sessionImplementor -> {
@Override // Construct the update contexts
public void doBeforeTransactionCompletion(final SessionImplementor sessionImplementor) { final List<UpdateContext> contexts = getUpdateContexts(
final Queryable productionEntityQueryable = getQueryable( entityName, sessionImplementor ); entityName,
final Queryable rootProductionEntityQueryable = getQueryable( auditedEntityName,
productionEntityQueryable.getRootEntityName(), sessionImplementor sessionImplementor,
); configuration,
final Queryable auditedEntityQueryable = getQueryable( auditedEntityName, sessionImplementor ); id,
final Queryable rootAuditedEntityQueryable = getQueryable( revision
auditedEntityQueryable.getRootEntityName(), sessionImplementor
); );
final String updateTableName; if ( contexts.isEmpty() ) {
if ( UnionSubclassEntityPersister.class.isInstance( rootProductionEntityQueryable ) ) { throw new AuditException(
// this is the condition causing all the problems in terms of the generated SQL UPDATE String.format(
// the problem being that we currently try to update the in-line view made up of the union query Locale.ENGLISH,
// "Failed to build update contexts for entity %s and id %s",
// this is extremely hacky means to get the root table name for the union subclass style entities. auditedEntityName,
// hacky because it relies on internal behavior of UnionSubclassEntityPersister id
// !!!!!! NOTICE - using subclass persister, not root !!!!!! )
updateTableName = auditedEntityQueryable.getSubclassTableName( 0 );
}
else {
updateTableName = rootAuditedEntityQueryable.getTableName();
}
final Type revisionInfoIdType = sessionImplementor.getFactory().getMetamodel().entityPersister( revisionInfoEntityName ).getIdentifierType();
final String revEndColumnName = rootAuditedEntityQueryable.toColumns( configuration.getRevisionEndFieldName() )[0];
final boolean isRevisionEndTimestampEnabled = configuration.isRevisionEndTimestampEnabled();
// update audit_ent set REVEND = ? [, REVEND_TSTMP = ?] where (prod_ent_id) = ? and REV <> ? and REVEND is null
final Update update = new Update( sessionImplementor.getFactory().getJdbcServices().getDialect() ).setTableName( updateTableName );
// set REVEND = ?
update.addColumn( revEndColumnName );
// set [, REVEND_TSTMP = ?]
if ( isRevisionEndTimestampEnabled ) {
update.addColumn(
rootAuditedEntityQueryable.toColumns( configuration.getRevisionEndTimestampFieldName() )[0]
); );
} }
// where (prod_ent_id) = ? for ( UpdateContext context : contexts ) {
update.addPrimaryKeyColumns( rootProductionEntityQueryable.getIdentifierColumnNames() ); final int rows = executeUpdate( sessionImplementor, context );
// where REV <> ? if ( rows != 1 ) {
update.addWhereColumn( final RevisionType revisionType = getRevisionType( configuration, data );
rootAuditedEntityQueryable.toColumns( configuration.getRevisionNumberPath() )[0], "<> ?" if ( !reuseEntityIdentifier || revisionType != RevisionType.ADD ) {
throw new AuditException(
String.format(
Locale.ENGLISH,
"Cannot update previous revision for entity %s and id %s (%s rows modified).",
auditedEntityName,
id,
rows
)
); );
// where REVEND is null
update.addWhereColumn( revEndColumnName, " is null" );
// Now lets execute the sql...
final String updateSql = update.toStatementString();
int rowCount = sessionImplementor.doReturningWork(
new ReturningWork<Integer>() {
@Override
public Integer execute(Connection connection) throws SQLException {
PreparedStatement preparedStatement = sessionImplementor
.getJdbcCoordinator().getStatementPreparer().prepareStatement( updateSql );
try {
int index = 1;
// set REVEND = ?
final Number revisionNumber = configuration.getEnversService()
.getRevisionInfoNumberReader()
.getRevisionNumber( revision );
revisionInfoIdType.nullSafeSet(
preparedStatement, revisionNumber, index, sessionImplementor
);
index += revisionInfoIdType.getColumnSpan( sessionImplementor.getFactory() );
// set [, REVEND_TSTMP = ?]
if ( isRevisionEndTimestampEnabled ) {
final Object revEndTimestampObj = revisionTimestampGetter.get( revision );
final Object revEndValue = getRevEndTimestampValue( configuration, revEndTimestampObj );
final Type revEndTsType = rootAuditedEntityQueryable.getPropertyType(
configuration.getRevisionEndTimestampFieldName()
);
revEndTsType.nullSafeSet( preparedStatement, revEndValue, index, sessionImplementor );
index += revEndTsType.getColumnSpan( sessionImplementor.getFactory() );
} }
// where (prod_ent_id) = ?
final Type idType = rootProductionEntityQueryable.getIdentifierType();
idType.nullSafeSet( preparedStatement, id, index, sessionImplementor );
index += idType.getColumnSpan( sessionImplementor.getFactory() );
// where REV <> ?
final Type revType = rootAuditedEntityQueryable.getPropertyType(
configuration.getRevisionNumberPath()
);
revType.nullSafeSet( preparedStatement, revisionNumber, index, sessionImplementor );
// where REVEND is null
// nothing to bind....
return sessionImplementor
.getJdbcCoordinator().getResultSetReturn().executeUpdate( preparedStatement );
}
finally {
sessionImplementor.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release(
preparedStatement
);
sessionImplementor.getJdbcCoordinator().afterStatementExecution();
}
}
}
);
if ( rowCount != 1 && ( !reuseEntityIdentifier || ( getRevisionType( configuration, data ) != RevisionType.ADD ) ) ) {
throw new RuntimeException(
"Cannot update previous revision for entity " + auditedEntityName + " and id " + id
);
} }
} }
} ); } );
@ -512,4 +450,239 @@ public class ValidityAuditStrategy implements AuditStrategy {
} }
return false; return false;
} }
/**
* Executes the {@link UpdateContext} within the scope of the specified session.
*
* @param session the session
* @param context the update context to be executed
* @return the number of rows affected by the operation
*/
private int executeUpdate(SessionImplementor session, UpdateContext context) {
final String sql = context.toStatementString();
final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator();
final PreparedStatement statement = jdbcCoordinator.getStatementPreparer().prepareStatement( sql );
return session.doReturningWork(
connection -> {
try {
int index = 1;
for ( QueryParameterBinding binding : context.getBindings() ) {
index += binding.bind( index, statement, session );
}
int result = jdbcCoordinator.getResultSetReturn().executeUpdate( statement );
return result;
}
finally {
jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( statement );
jdbcCoordinator.afterStatementExecution();
}
}
);
}
private List<UpdateContext> getUpdateContexts(
String entityName,
String auditEntityName,
SessionImplementor session,
Configuration configuration,
Object id,
Object revision) {
Queryable entity = getQueryable( entityName, session );
final List<UpdateContext> contexts = new ArrayList<>( 0 );
// HHH-9062 - update inherited
if ( configuration.isRevisionEndTimestampEnabled() && !configuration.isRevisionEndTimestampUseLegacyPlacement() ) {
if ( entity instanceof JoinedSubclassEntityPersister) {
// iterate subclasses, excluding root
while ( entity.getEntityMetamodel().getSuperclass() != null ) {
contexts.add(
getNonRootUpdateContext(
entityName,
auditEntityName,
session,
configuration,
id,
revision
)
);
entityName = entity.getEntityMetamodel().getSuperclass();
auditEntityName = configuration.getAuditEntityName( entityName );
entity = getQueryable( entityName, session );
}
}
}
// add root
contexts.add(
getUpdateContext(
entityName,
auditEntityName,
session,
configuration,
id,
revision
)
);
return contexts;
}
private UpdateContext getUpdateContext(
String entityName,
String auditEntityName,
SessionImplementor session,
Configuration configuration,
Object id,
Object revision) {
final Queryable entity = getQueryable( entityName, session );
final Queryable rootEntity = getQueryable( entity.getRootEntityName(), session );
final Queryable auditEntity = getQueryable( auditEntityName, session );
final Queryable rootAuditEntity = getQueryable( auditEntity.getRootEntityName(), session );
final Queryable revisionEntity = getQueryable( configuration.getRevisionInfo().getRevisionInfoEntityName(), session );
final Number revisionNumber = getRevisionNumber( configuration, revision );
final Type revisionNumberType = revisionEntity.getIdentifierType();
// The expected SQL is an update statement as follows:
// UPDATE audited_entity SET REVEND = ? [, REVEND_TSTMP = ?] WHERE (entity_id) = ? AND REV <> ? AND REVEND is null
final UpdateContext context = new UpdateContext( session.getFactory() );
context.setTableName( getUpdateTableName( rootEntity, rootAuditEntity, auditEntity ) );
// Apply "SET REVEND = ?" portion of the SQL
final String revEndColumnName = configuration.getRevisionEndFieldName();
context.addColumn( rootAuditEntity.toColumns( revEndColumnName )[ 0 ] );
context.bind( revisionNumber, revisionNumberType );
if ( configuration.isRevisionEndTimestampEnabled() ) {
final String revEndTimestampColumnName = configuration.getRevisionEndTimestampFieldName();
final Type revEndTimestampType = rootAuditEntity.getPropertyType( revEndTimestampColumnName );
final Object revisionTimestamp = revisionTimestampGetter.get( revision );
// Apply optional "[, REVEND_TSTMP = ?]" portion of the SQL
context.addColumn( rootAuditEntity.toColumns( revEndTimestampColumnName )[ 0 ] );
context.bind( getRevEndTimestampValue( configuration, revisionTimestamp ), revEndTimestampType );
}
// Apply "WHERE (entity_id) = ?"
context.addPrimaryKeyColumns( entity.getIdentifierColumnNames() );
context.bind( id, entity.getIdentifierType() );
// Apply "AND REV <> ?"
final String path = configuration.getRevisionNumberPath();
context.addWhereColumn( rootAuditEntity.toColumns( path )[ 0 ], " <> ?" );
context.bind( revisionNumber, rootAuditEntity.getPropertyType( path ) );
// Apply "AND REVEND is null"
context.addWhereColumn( auditEntity.toColumns( revEndColumnName )[ 0 ], " is null" );
return context;
}
/**
* Creates the update context used to modify the revision end timestamp values for a non-root entity.
* This is only used to set the revision end timestamp for joined inheritance non-root entity mappings.
*
* @param entityName the entity name
* @param auditEntityName the audited entity name
* @param session the session
* @param configuration the configuration
* @param id the entity identifier
* @param revision the revision entity
* @return the created update context instance, never {@code null}.
*/
private UpdateContext getNonRootUpdateContext(
String entityName,
String auditEntityName,
SessionImplementor session,
Configuration configuration,
Object id,
Object revision) {
final Queryable entity = getQueryable( entityName, session );
final Queryable auditEntity = getQueryable( auditEntityName, session );
final String revEndTimestampColumnName = configuration.getRevisionEndTimestampFieldName();
final Type revEndTimestampType = auditEntity.getPropertyType( revEndTimestampColumnName );
// The expected SQL is an update statement as follows:
// UPDATE audited_entity SET REVEND_TSTMP = ? WHERE (entity_id) = ? AND REV <> ? AND REVEND_TSMTP is null
final UpdateContext context = new UpdateContext( session.getFactory() );
context.setTableName( getUpdateTableName( entity, auditEntity, auditEntity ) );
// Apply "SET REVEND_TSTMP = ?" portion of the SQL
final Object revisionTimestamp = revisionTimestampGetter.get( revision );
context.addColumn( auditEntity.toColumns( revEndTimestampColumnName )[ 0 ] );
context.bind( getRevEndTimestampValue( configuration, revisionTimestamp ), revEndTimestampType );
// Apply "WHERE (entity_id) = ? AND REV <> ?" portion of the SQL
final Number revisionNumber = getRevisionNumber( configuration, revision );
// Apply "WHERE (entity_id) = ?"
context.addPrimaryKeyColumns( entity.getIdentifierColumnNames() );
context.bind( id, entity.getIdentifierType() );
// Apply "AND REV <> ?"
context.addWhereColumn( configuration.getRevisionFieldName(), " <> ?" );
context.bind( revisionNumber, auditEntity.getPropertyType( configuration.getRevisionNumberPath() ) );
// Apply "AND REVEND_TSTMP is null"
context.addWhereColumn( auditEntity.toColumns( revEndTimestampColumnName )[ 0 ], " is null" );
session.createQuery( "From " + auditEntityName + " WHERE originalId.REV.id <> " + revisionNumber ).list();
return context;
}
private Number getRevisionNumber(Configuration configuration, Object revisionEntity) {
final RevisionInfoNumberReader reader = configuration.getRevisionInfo().getRevisionInfoNumberReader();
return reader.getRevisionNumber( revisionEntity );
}
private String getUpdateTableName(Queryable rootEntity, Queryable rootAuditEntity, Queryable auditEntity) {
if ( UnionSubclassEntityPersister.class.isInstance( rootEntity ) ) {
// This is the condition causing all the problems of the generated SQL update;
// the problem being that we currently try to update the inline view made of the union query.
//
// This is hacky to get the root table name for the union subclass style entities because
// it relies on internal behavior of UnionSubclassEntityPersister.
return auditEntity.getSubclassTableName( 0 );
}
return rootAuditEntity.getTableName();
}
/**
* An {@link Update} that can also track parameter bindings.
*/
private static class UpdateContext extends Update {
private final List<QueryParameterBinding> bindings = new ArrayList<>( 0 );
public UpdateContext(SessionFactoryImplementor sessionFactory) {
super ( sessionFactory.getJdbcServices().getDialect() );
}
public List<QueryParameterBinding> getBindings() {
return bindings;
}
public void bind(Object value, Type type) {
bindings.add( new QueryParameterBinding( value, type ) );
}
}
private static class QueryParameterBinding {
private final Type type;
private final Object value;
public QueryParameterBinding(Object value, Type type) {
this.type = type;
this.value = value;
}
public int bind(int index, PreparedStatement statement, SessionImplementor session) throws SQLException {
type.nullSafeSet( statement, value, index, session );
return type.getColumnSpan( session.getSessionFactory() );
}
}
} }

View File

@ -22,16 +22,18 @@ public class MappingContext {
private final Configuration configuration; private final Configuration configuration;
private final String revisionInfoPropertyType; private final String revisionInfoPropertyType;
private final String revisionInfoExplicitTypeName; private final String revisionInfoExplicitTypeName;
private final boolean revisionEndTimestampOnly;
public MappingContext( public MappingContext(
PersistentEntity mapping, PersistentEntity mapping,
Configuration configuration, Configuration configuration,
String revisionInfoPropertyType, String revisionInfoPropertyType,
String revisionInfoExplicitTypeName) { String revisionInfoExplicitTypeName,
boolean revisionEndTimestampOnly) {
this.mapping = mapping; this.mapping = mapping;
this.configuration = configuration; this.configuration = configuration;
this.revisionInfoPropertyType = revisionInfoPropertyType; this.revisionInfoPropertyType = revisionInfoPropertyType;
this.revisionInfoExplicitTypeName = revisionInfoExplicitTypeName; this.revisionInfoExplicitTypeName = revisionInfoExplicitTypeName;
this.revisionEndTimestampOnly = revisionEndTimestampOnly;
} }
public PersistentEntity getEntityMapping() { public PersistentEntity getEntityMapping() {
@ -49,4 +51,8 @@ public class MappingContext {
public String getRevisionInfoExplicitTypeName() { public String getRevisionInfoExplicitTypeName() {
return revisionInfoExplicitTypeName; return revisionInfoExplicitTypeName;
} }
public boolean isRevisionEndTimestampOnly() {
return revisionEndTimestampOnly;
}
} }

View File

@ -0,0 +1,75 @@
/*
* 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.strategy;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import java.util.Date;
import java.util.List;
import java.util.Map;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.SybaseASE15Dialect;
import org.hibernate.envers.configuration.EnversSettings;
import org.hibernate.envers.enhanced.SequenceIdRevisionEntity;
import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase;
/**
* @author Chris Cranford
*/
public abstract class AbstractRevisionEndTimestampTest extends BaseEnversJPAFunctionalTestCase {
private static final String TIMESTAMP_FIELD = "REVEND_TSTMP";
@Override
@SuppressWarnings("unchecked")
public void addConfigOptions(Map options) {
options.put( EnversSettings.AUDIT_TABLE_SUFFIX, "_AUD" );
options.put( EnversSettings.AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_FIELD_NAME, TIMESTAMP_FIELD );
options.put( EnversSettings.AUDIT_STRATEGY_VALIDITY_STORE_REVEND_TIMESTAMP, "true" );
options.put( EnversSettings.AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_LEGACY_PLACEMENT, "false" );
}
@SuppressWarnings("unchecked")
protected List<Map<String, Object>> getRevisions(Class<?> clazz, Integer id) {
String sql = String.format( "SELECT e FROM %s_AUD e WHERE e.originalId.id = :id", clazz.getName() );
return getEntityManager().createQuery( sql ).setParameter( "id", id ).getResultList();
}
protected void verifyRevisionEndTimestampsInSubclass(Class<?> clazz, Integer id) {
final List<Map<String, Object>> entities = getRevisions( clazz, id );
for ( Map<String, Object> entity : entities ) {
Object timestampParentClass = entity.get( TIMESTAMP_FIELD );
Object timestampSubclass = entity.get( TIMESTAMP_FIELD + "_" + clazz.getSimpleName() + "_AUD" );
SequenceIdRevisionEntity revisionEnd = (SequenceIdRevisionEntity) entity.get( "REVEND" );
if ( timestampParentClass == null ) {
// if the parent class has no revision end timestamp, verify that the child does not have a value
// as well as that the revision end field is also null.
assertNull( timestampSubclass );
assertNull( revisionEnd );
}
else {
// Verify that the timestamp in the revision entity matches that in the parent entity's
// revision end timestamp field as well.
final Date timestamp = (Date) timestampParentClass;
final Dialect dialect = getDialect();
if ( dialect instanceof SybaseASE15Dialect) {
// Sybase DATETIME are accurate to 1/300 second on platforms that support that level of
// granularity.
assertEquals( timestamp.getTime() / 1000.0, revisionEnd.getTimestamp() / 1000.0, 1.0 / 300.0 );
}
else {
assertEquals( timestamp.getTime(), revisionEnd.getTimestamp() );
}
// make sure both parent and child have the same values.
assertEquals( timestampParentClass, timestampSubclass );
}
}
}
}

View File

@ -0,0 +1,282 @@
/*
* 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.strategy;
import org.hibernate.envers.Audited;
import org.hibernate.envers.strategy.ValidityAuditStrategy;
import org.hibernate.orm.test.envers.Priority;
import org.junit.Test;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.envers.RequiresAuditStrategy;
import jakarta.persistence.DiscriminatorColumn;
import jakarta.persistence.DiscriminatorValue;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Inheritance;
import jakarta.persistence.InheritanceType;
/**
* @author Chris Cranford
*/
@TestForIssue( jiraKey = "HHH-9092" )
@RequiresAuditStrategy( ValidityAuditStrategy.class )
public class RevisionEndTimestampJoinedInheritanceTest extends AbstractRevisionEndTimestampTest {
private Integer fullTimeEmployeeId;
private Integer contractorId;
private Integer executiveId;
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { Employee.class, FullTimeEmployee.class, Contractor.class, Executive.class };
}
@Test
@Priority(10)
public void initData() {
EntityManager entityManager = getEntityManager();
try {
FullTimeEmployee fullTimeEmployee = new FullTimeEmployee( "Employee", 50000 );
Contractor contractor = new Contractor( "Contractor", 45 );
Executive executive = new Executive( "Executive", 100000, "CEO" );
// Revision 1
entityManager.getTransaction().begin();
entityManager.persist( fullTimeEmployee );
entityManager.persist( contractor );
entityManager.persist( executive );
entityManager.getTransaction().commit();
// Revision 2 - raises for everyone!
entityManager.getTransaction().begin();
fullTimeEmployee.setSalary( 60000 );
contractor.setHourlyRate( 47 );
executive.setSalary( 125000 );
entityManager.getTransaction().commit();
fullTimeEmployeeId = fullTimeEmployee.getId();
contractorId = contractor.getId();
executiveId = executive.getId();
}
catch ( Exception e ) {
if ( entityManager.getTransaction().isActive() ) {
entityManager.getTransaction().rollback();
}
throw e;
}
finally {
entityManager.close();
}
}
@Test
public void testRevisionEndTimestamps() {
verifyRevisionEndTimestampsInSubclass(FullTimeEmployee.class, fullTimeEmployeeId );
verifyRevisionEndTimestampsInSubclass(Contractor.class, contractorId );
verifyRevisionEndTimestampsInSubclass(Executive.class, executiveId );
}
@Audited
@Entity(name = "Employee")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(length = 255)
@DiscriminatorValue("EMP")
public static class Employee {
@Id
@GeneratedValue
private Integer id;
private String name;
Employee() {
}
Employee(String name) {
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;
}
@Override
public int hashCode() {
int result = ( id != null ? id.hashCode() : 0 );
result = result * 31 + ( name != null ? name.hashCode() : 0 );
return result;
}
@Override
public boolean equals(Object object) {
if ( this == object ) {
return true;
}
if ( object == null || !( object instanceof Employee ) ) {
return false;
}
Employee that = (Employee) object;
if ( id != null ? !id.equals( that.id ) : that.id != null ) {
return false;
}
return !( name != null ? !name.equals( that.name ) : that.name != null );
}
}
@Audited
@Entity(name = "FullTimeEmployee")
@DiscriminatorValue("FT")
public static class FullTimeEmployee extends Employee {
private Integer salary;
FullTimeEmployee() {
}
FullTimeEmployee(String name, Integer salary) {
super( name );
this.salary = salary;
}
public Integer getSalary() {
return salary;
}
public void setSalary(Integer salary) {
this.salary = salary;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = result * 31 + ( salary != null ? salary.hashCode() : 0 );
return result;
}
@Override
public boolean equals(Object object) {
if ( this == object ) {
return true;
}
if ( object == null || !( object instanceof FullTimeEmployee ) ) {
return false;
}
if ( !super.equals( object ) ) {
return false;
}
FullTimeEmployee that = (FullTimeEmployee) object;
return !( salary != null ? !salary.equals( that.salary ) : that.salary != null );
}
}
@Audited
@Entity(name = "Contractor")
@DiscriminatorValue("CONTRACT")
public static class Contractor extends Employee {
private Integer hourlyRate;
Contractor() {
}
Contractor(String name, Integer hourlyRate) {
super( name );
this.hourlyRate = hourlyRate;
}
public Integer getHourlyRate() {
return hourlyRate;
}
public void setHourlyRate(Integer hourlyRate) {
this.hourlyRate = hourlyRate;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = result * 31 + ( hourlyRate != null ? hourlyRate.hashCode() : 0 );
return result;
}
@Override
public boolean equals(Object object) {
if ( this == object ) {
return true;
}
if ( object == null || !( object instanceof Contractor ) ) {
return false;
}
if ( !super.equals( object ) ) {
return false;
}
Contractor that = (Contractor) object;
return !( hourlyRate != null ? !hourlyRate.equals( that.hourlyRate ) : that.hourlyRate != null );
}
}
@Audited
@Entity(name = "Executive")
@DiscriminatorValue("EXEC")
public class Executive extends FullTimeEmployee {
private String title;
Executive() {
}
Executive(String name, Integer salary, String title) {
super( name, salary );
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
@Override
public int hashCode() {
int result = super.hashCode();
result = result * 31 + ( title != null ? title.hashCode() : 0 );
return result;
}
@Override
public boolean equals(Object object) {
if ( this == object ) {
return true;
}
if ( object == null || !( object instanceof Executive ) ) {
return false;
}
if ( !super.equals( object ) ) {
return false;
}
Executive that = (Executive) object;
return !( title != null ? !title.equals( that.title ) : that.title != null );
}
}
}