HHH-9062 Allow validity audit strategy to store revision end timestamps on joined subclass audit tables.
This commit is contained in:
parent
9124fd84b4
commit
dbecdc41ac
|
@ -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.
|
||||
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` )::
|
||||
Boolean flag that determines the strategy of revision number generation.
|
||||
Default implementation of revision entity uses native identifier generator.
|
||||
|
|
|
@ -88,6 +88,7 @@ public class Configuration {
|
|||
private final String embeddableSetOrdinalPropertyName;
|
||||
private final boolean revisionEndTimestampEnabled;
|
||||
private final boolean revisionEndTimestampNumeric;
|
||||
private final boolean revisionEndTimestampUseLegacyPlacement;
|
||||
|
||||
private final Map<String, String> customAuditTableNames = new HashMap<>();
|
||||
|
||||
|
@ -156,10 +157,15 @@ public class Configuration {
|
|||
EnversSettings.AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_NUMERIC,
|
||||
false
|
||||
);
|
||||
revisionEndTimestampUseLegacyPlacement = configProps.getBoolean(
|
||||
EnversSettings.AUDIT_STRATEGY_VALIDITY_REVEND_TIMESTAMP_LEGACY_PLACEMENT,
|
||||
true
|
||||
);
|
||||
}
|
||||
else {
|
||||
revisionEndTimestampFieldName = null;
|
||||
revisionEndTimestampNumeric = false;
|
||||
revisionEndTimestampUseLegacyPlacement = true;
|
||||
}
|
||||
|
||||
embeddableSetOrdinalPropertyName = configProps.getString(
|
||||
|
@ -228,6 +234,10 @@ public class Configuration {
|
|||
return revisionEndTimestampNumeric;
|
||||
}
|
||||
|
||||
public boolean isRevisionEndTimestampUseLegacyPlacement() {
|
||||
return revisionEndTimestampUseLegacyPlacement;
|
||||
}
|
||||
|
||||
public String getDefaultCatalogName() {
|
||||
return defaultCatalogName;
|
||||
}
|
||||
|
|
|
@ -118,6 +118,16 @@ public interface EnversSettings {
|
|||
*/
|
||||
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}.
|
||||
*/
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.hibernate.envers.boot.EnversMappingException;
|
|||
import org.hibernate.envers.boot.model.AttributeContainer;
|
||||
import org.hibernate.envers.boot.model.BasicAttribute;
|
||||
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.spi.EnversMetadataBuildingContext;
|
||||
import org.hibernate.envers.configuration.Configuration;
|
||||
|
@ -110,11 +111,27 @@ public abstract class AbstractMetadataGenerator {
|
|||
entity,
|
||||
metadataBuildingContext.getConfiguration(),
|
||||
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) {
|
||||
container.addAttribute(
|
||||
new BasicAttribute(
|
||||
|
|
|
@ -342,6 +342,10 @@ public final class AuditMetadataGenerator extends AbstractMetadataGenerator {
|
|||
// HHH-7940 - New synthetic property support for @IndexColumn/@OrderColumn dynamic properties
|
||||
addSynthetics( entity, auditingData, propertyMapper, mappingData, persistentClass.getEntityName() );
|
||||
|
||||
if ( !configuration.isRevisionEndTimestampUseLegacyPlacement() ) {
|
||||
addAuditStrategyRevisionEndTimestampOnly( entity );
|
||||
}
|
||||
|
||||
mappingData.addMapping( entity );
|
||||
|
||||
// Storing the generated configuration
|
||||
|
|
|
@ -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.REVISION_PARAMETER;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Types;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import org.hibernate.LockOptions;
|
||||
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.SharedSessionContractImplementor;
|
||||
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.configuration.Configuration;
|
||||
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.relation.MiddleComponentData;
|
||||
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.tools.query.Parameters;
|
||||
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.MappingContext;
|
||||
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.UnionSubclassEntityPersister;
|
||||
import org.hibernate.property.access.spi.Getter;
|
||||
|
@ -91,21 +95,24 @@ public class ValidityAuditStrategy implements AuditStrategy {
|
|||
|
||||
@Override
|
||||
public void addAdditionalColumns(MappingContext mappingContext) {
|
||||
final ManyToOneAttribute revEndMapping = new ManyToOneAttribute(
|
||||
mappingContext.getConfiguration().getRevisionEndFieldName(),
|
||||
mappingContext.getRevisionInfoPropertyType(),
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
mappingContext.getRevisionInfoExplicitTypeName()
|
||||
);
|
||||
if ( !mappingContext.isRevisionEndTimestampOnly() ) {
|
||||
// Add revision end field since mapping is not requesting only the timestamp.
|
||||
final ManyToOneAttribute revEndMapping = new ManyToOneAttribute(
|
||||
mappingContext.getConfiguration().getRevisionEndFieldName(),
|
||||
mappingContext.getRevisionInfoPropertyType(),
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
mappingContext.getRevisionInfoExplicitTypeName()
|
||||
);
|
||||
|
||||
RevisionInfoHelper.addOrModifyColumn(
|
||||
revEndMapping,
|
||||
mappingContext.getConfiguration().getRevisionEndFieldName()
|
||||
);
|
||||
RevisionInfoHelper.addOrModifyColumn(
|
||||
revEndMapping,
|
||||
mappingContext.getConfiguration().getRevisionEndFieldName()
|
||||
);
|
||||
|
||||
mappingContext.getEntityMapping().addAttribute( revEndMapping );
|
||||
mappingContext.getEntityMapping().addAttribute( revEndMapping );
|
||||
}
|
||||
|
||||
if ( mappingContext.getConfiguration().isRevisionEndTimestampEnabled() ) {
|
||||
// add a column for the timestamp of the end revision
|
||||
|
@ -116,16 +123,27 @@ public class ValidityAuditStrategy implements AuditStrategy {
|
|||
else {
|
||||
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(
|
||||
mappingContext.getConfiguration().getRevisionEndTimestampFieldName(),
|
||||
revEndTimestampPropertyName,
|
||||
revisionInfoTimestampTypeName,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
);
|
||||
revEndTimestampMapping.addColumn(
|
||||
new Column( mappingContext.getConfiguration().getRevisionEndTimestampFieldName() )
|
||||
);
|
||||
revEndTimestampMapping.addColumn( new Column( revEndTimestampColumnName ) );
|
||||
mappingContext.getEntityMapping().addAttribute( revEndTimestampMapping );
|
||||
}
|
||||
}
|
||||
|
@ -139,7 +157,6 @@ public class ValidityAuditStrategy implements AuditStrategy {
|
|||
final Object data,
|
||||
final Object revision) {
|
||||
final String auditedEntityName = configuration.getAuditEntityName( entityName );
|
||||
final String revisionInfoEntityName = configuration.getRevisionInfo().getRevisionInfoEntityName();
|
||||
|
||||
// Save the audit data
|
||||
session.save( auditedEntityName, data );
|
||||
|
@ -154,122 +171,43 @@ public class ValidityAuditStrategy implements AuditStrategy {
|
|||
final boolean reuseEntityIdentifier = configuration.isAllowIdentifierReuse();
|
||||
if ( reuseEntityIdentifier || getRevisionType( configuration, data ) != RevisionType.ADD ) {
|
||||
// Register transaction completion process to guarantee execution of UPDATE statement after INSERT.
|
||||
( (EventSource) session ).getActionQueue().registerProcess( new BeforeTransactionCompletionProcess() {
|
||||
@Override
|
||||
public void doBeforeTransactionCompletion(final SessionImplementor sessionImplementor) {
|
||||
final Queryable productionEntityQueryable = getQueryable( entityName, sessionImplementor );
|
||||
final Queryable rootProductionEntityQueryable = getQueryable(
|
||||
productionEntityQueryable.getRootEntityName(), sessionImplementor
|
||||
);
|
||||
final Queryable auditedEntityQueryable = getQueryable( auditedEntityName, sessionImplementor );
|
||||
final Queryable rootAuditedEntityQueryable = getQueryable(
|
||||
auditedEntityQueryable.getRootEntityName(), sessionImplementor
|
||||
( (EventSource) session ).getActionQueue().registerProcess( sessionImplementor -> {
|
||||
// Construct the update contexts
|
||||
final List<UpdateContext> contexts = getUpdateContexts(
|
||||
entityName,
|
||||
auditedEntityName,
|
||||
sessionImplementor,
|
||||
configuration,
|
||||
id,
|
||||
revision
|
||||
);
|
||||
|
||||
if ( contexts.isEmpty() ) {
|
||||
throw new AuditException(
|
||||
String.format(
|
||||
Locale.ENGLISH,
|
||||
"Failed to build update contexts for entity %s and id %s",
|
||||
auditedEntityName,
|
||||
id
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
final String updateTableName;
|
||||
if ( UnionSubclassEntityPersister.class.isInstance( rootProductionEntityQueryable ) ) {
|
||||
// this is the condition causing all the problems in terms of the generated SQL UPDATE
|
||||
// the problem being that we currently try to update the in-line view made up of the union query
|
||||
//
|
||||
// this is extremely hacky means to get the root table name for the union subclass style entities.
|
||||
// hacky because it relies on internal behavior of UnionSubclassEntityPersister
|
||||
// !!!!!! 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) = ?
|
||||
update.addPrimaryKeyColumns( rootProductionEntityQueryable.getIdentifierColumnNames() );
|
||||
// where REV <> ?
|
||||
update.addWhereColumn(
|
||||
rootAuditedEntityQueryable.toColumns( configuration.getRevisionNumberPath() )[0], "<> ?"
|
||||
);
|
||||
// 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
|
||||
);
|
||||
for ( UpdateContext context : contexts ) {
|
||||
final int rows = executeUpdate( sessionImplementor, context );
|
||||
if ( rows != 1 ) {
|
||||
final RevisionType revisionType = getRevisionType( configuration, data );
|
||||
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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} );
|
||||
|
@ -512,4 +450,239 @@ public class ValidityAuditStrategy implements AuditStrategy {
|
|||
}
|
||||
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() );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,16 +22,18 @@ public class MappingContext {
|
|||
private final Configuration configuration;
|
||||
private final String revisionInfoPropertyType;
|
||||
private final String revisionInfoExplicitTypeName;
|
||||
|
||||
private final boolean revisionEndTimestampOnly;
|
||||
public MappingContext(
|
||||
PersistentEntity mapping,
|
||||
Configuration configuration,
|
||||
String revisionInfoPropertyType,
|
||||
String revisionInfoExplicitTypeName) {
|
||||
String revisionInfoExplicitTypeName,
|
||||
boolean revisionEndTimestampOnly) {
|
||||
this.mapping = mapping;
|
||||
this.configuration = configuration;
|
||||
this.revisionInfoPropertyType = revisionInfoPropertyType;
|
||||
this.revisionInfoExplicitTypeName = revisionInfoExplicitTypeName;
|
||||
this.revisionEndTimestampOnly = revisionEndTimestampOnly;
|
||||
}
|
||||
|
||||
public PersistentEntity getEntityMapping() {
|
||||
|
@ -49,4 +51,8 @@ public class MappingContext {
|
|||
public String getRevisionInfoExplicitTypeName() {
|
||||
return revisionInfoExplicitTypeName;
|
||||
}
|
||||
|
||||
public boolean isRevisionEndTimestampOnly() {
|
||||
return revisionEndTimestampOnly;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue