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

View File

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

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";
/**
* 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}.
*/

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.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(

View File

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

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

View File

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

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