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