From 87211e7cd48cc8a54d6dcad037e5dea09c2daa3f Mon Sep 17 00:00:00 2001 From: Gavin King Date: Sat, 1 Oct 2022 15:28:41 +0200 Subject: [PATCH] HHH-15564 add @SecondaryRow annotation and HHH-15565 completely remove the notion of subsequent select fetching for secondary tables ... and deprecate the member of @Table which turns this on, since the actual underlying functionality is missing in H6! + some misc code cleanup in AbstractEntityPersister --- .../hibernate/annotations/SecondaryRow.java | 48 +++++ .../hibernate/annotations/SecondaryRows.java | 24 +++ .../java/org/hibernate/annotations/Table.java | 20 +- .../org/hibernate/annotations/Tables.java | 4 +- .../source/internal/hbm/ModelBinder.java | 1 - .../org/hibernate/cfg/OneToOneSecondPass.java | 1 - .../cfg/annotations/EntityBinder.java | 41 +++- .../main/java/org/hibernate/mapping/Join.java | 8 - .../entity/AbstractEntityPersister.java | 196 +++++++++--------- .../entity/JoinedSubclassEntityPersister.java | 24 +-- .../entity/SingleTableEntityPersister.java | 20 +- .../resource/jdbc/spi/StatementInspector.java | 5 +- .../tuple/entity/EntityMetamodel.java | 6 +- .../orm/test/secondarytable/Record.java | 26 +++ .../test/secondarytable/SecondaryRowTest.java | 100 +++++++++ .../test/secondarytable/SpecialRecord.java | 19 ++ 16 files changed, 380 insertions(+), 163 deletions(-) create mode 100644 hibernate-core/src/main/java/org/hibernate/annotations/SecondaryRow.java create mode 100644 hibernate-core/src/main/java/org/hibernate/annotations/SecondaryRows.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/Record.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/SecondaryRowTest.java create mode 100644 hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/SpecialRecord.java diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SecondaryRow.java b/hibernate-core/src/main/java/org/hibernate/annotations/SecondaryRow.java new file mode 100644 index 0000000000..161a4c15ec --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SecondaryRow.java @@ -0,0 +1,48 @@ +/* + * 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 . + */ +package org.hibernate.annotations; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Specifies how the row of a {@link jakarta.persistence.SecondaryTable} should be managed. + * + * @see jakarta.persistence.SecondaryTable + * + * @author Gavin King + */ +@Target(TYPE) +@Retention(RUNTIME) +@Repeatable(SecondaryRows.class) +public @interface SecondaryRow { + /** + * The name of the secondary table, as specified by + * {@link jakarta.persistence.SecondaryTable#name()}. + */ + String table() default ""; + + /** + * If enabled, Hibernate will never insert or update the columns of the secondary table. + *

+ * This setting is useful if data in the secondary table belongs to some other entity, + * or if it is maintained externally to Hibernate. + */ + boolean owned() default true; + + /** + * Unless disabled, specifies that no row should be inserted in the secondary table if + * all the columns of the secondary table would be null. Furthermore, an outer join will + * always be used to read the row. Thus, by default, Hibernate avoids creating a row + * containing only null column values. + */ + boolean optional() default true; +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/SecondaryRows.java b/hibernate-core/src/main/java/org/hibernate/annotations/SecondaryRows.java new file mode 100644 index 0000000000..89769341bd --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/annotations/SecondaryRows.java @@ -0,0 +1,24 @@ +/* + * 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 . + */ +package org.hibernate.annotations; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * A grouping of {@link SecondaryRows}. + * + * @author Gavin King + */ +@Target(TYPE) +@Retention(RUNTIME) +public @interface SecondaryRows { + SecondaryRow[] value(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Table.java b/hibernate-core/src/main/java/org/hibernate/annotations/Table.java index 1ac825757c..81f00dcaf0 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Table.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Table.java @@ -68,26 +68,19 @@ public @interface Table { ForeignKey foreignKey() default @ForeignKey( name="" ); /** - * Defines a fetching strategy for the secondary table. - *

- *

- * Only applies to secondary tables. + * @deprecated This setting has no effect in Hibernate 6 */ + @Deprecated(since = "6.2") FetchMode fetch() default FetchMode.JOIN; /** * If enabled, Hibernate will never insert or update the columns of the secondary table. *

* Only applies to secondary tables. + * + * @deprecated use {@link SecondaryRow#owned()} */ + @Deprecated(since = "6.2") boolean inverse() default false; /** @@ -96,7 +89,10 @@ public @interface Table { * by default, Hibernate avoids creating a row of null values. *

* Only applies to secondary tables. + * + * @deprecated use {@link SecondaryRow#optional()} */ + @Deprecated(since = "6.2") boolean optional() default true; /** diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/Tables.java b/hibernate-core/src/main/java/org/hibernate/annotations/Tables.java index cf047bce2a..40d0825ea5 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/Tables.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/Tables.java @@ -13,11 +13,11 @@ import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** - * A grouping of tables. + * A grouping of {@link Table}s. * * @author Emmanuel Bernard */ -@Target({TYPE}) +@Target(TYPE) @Retention(RUNTIME) public @interface Tables { /** diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java index 891aa18d55..32722f7ffd 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java @@ -1741,7 +1741,6 @@ public class ModelBinder { bindCustomSql( secondaryTableSource, secondaryTableJoin ); - secondaryTableJoin.setSequentialSelect( secondaryTableSource.getFetchStyle() == FetchStyle.SELECT ); secondaryTableJoin.setInverse( secondaryTableSource.isInverse() ); secondaryTableJoin.setOptional( secondaryTableSource.isOptional() ); diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/OneToOneSecondPass.java b/hibernate-core/src/main/java/org/hibernate/cfg/OneToOneSecondPass.java index 467bf2b79c..c61a1354ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/OneToOneSecondPass.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/OneToOneSecondPass.java @@ -279,7 +279,6 @@ public class OneToOneSecondPass implements SecondPass { //TODO support @ForeignKey join.setKey( key ); - join.setSequentialSelect( false ); //TODO support for inverse and optional join.setOptional( true ); //perhaps not quite per-spec, but a Good Thing anyway key.setCascadeDeleteEnabled( false ); diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/EntityBinder.java index 84975d4419..dc177ba801 100644 --- a/hibernate-core/src/main/java/org/hibernate/cfg/annotations/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/cfg/annotations/EntityBinder.java @@ -50,7 +50,6 @@ import org.hibernate.annotations.DiscriminatorFormula; import org.hibernate.annotations.DiscriminatorOptions; import org.hibernate.annotations.DynamicInsert; import org.hibernate.annotations.DynamicUpdate; -import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.Filter; import org.hibernate.annotations.Filters; import org.hibernate.annotations.ForeignKey; @@ -70,6 +69,8 @@ import org.hibernate.annotations.SQLDelete; import org.hibernate.annotations.SQLDeleteAll; import org.hibernate.annotations.SQLInsert; import org.hibernate.annotations.SQLUpdate; +import org.hibernate.annotations.SecondaryRow; +import org.hibernate.annotations.SecondaryRows; import org.hibernate.annotations.SelectBeforeUpdate; import org.hibernate.annotations.Subselect; import org.hibernate.annotations.Synchronize; @@ -1857,24 +1858,41 @@ public class EntityBinder { } private org.hibernate.annotations.Table findMatchingComplementaryTableAnnotation(Join join) { - String tableName = join.getTable().getQuotedName(); + final String tableName = join.getTable().getQuotedName(); org.hibernate.annotations.Table table = annotatedClass.getAnnotation( org.hibernate.annotations.Table.class ); - org.hibernate.annotations.Table matchingTable = null; if ( table != null && tableName.equals( table.appliesTo() ) ) { - matchingTable = table; + return table; } else { Tables tables = annotatedClass.getAnnotation( Tables.class ); if ( tables != null ) { for (org.hibernate.annotations.Table current : tables.value()) { if ( tableName.equals( current.appliesTo() ) ) { - matchingTable = current; - break; + return current; } } } + return null; + } + } + + private SecondaryRow findMatchingComplementarySecondaryRowAnnotation(Join join) { + final String tableName = join.getTable().getQuotedName(); + SecondaryRow row = annotatedClass.getAnnotation( SecondaryRow.class ); + if ( row != null && ( row.table().isEmpty() || tableName.equals( row.table() ) ) ) { + return row; + } + else { + SecondaryRows tables = annotatedClass.getAnnotation( SecondaryRows.class ); + if ( tables != null ) { + for ( SecondaryRow current : tables.value() ) { + if ( tableName.equals( current.table() ) ) { + return current; + } + } + } + return null; } - return matchingTable; } //Used for @*ToMany @JoinTable @@ -1964,9 +1982,13 @@ public class EntityBinder { //Has to do the work later because it needs persistentClass id! LOG.debugf( "Adding secondary table to entity %s -> %s", persistentClass.getEntityName(), join.getTable().getName() ); + SecondaryRow matchingRow = findMatchingComplementarySecondaryRowAnnotation( join ); org.hibernate.annotations.Table matchingTable = findMatchingComplementaryTableAnnotation( join ); - if ( matchingTable != null ) { - join.setSequentialSelect( FetchMode.JOIN != matchingTable.fetch() ); + if ( matchingRow != null ) { + join.setInverse( !matchingRow.owned() ); + join.setOptional( matchingRow.optional() ); + } + else if ( matchingTable != null ) { join.setInverse( matchingTable.inverse() ); join.setOptional( matchingTable.optional() ); String insertSql = matchingTable.sqlInsert().sql(); @@ -1996,7 +2018,6 @@ public class EntityBinder { } else { //default - join.setSequentialSelect( false ); join.setInverse( false ); join.setOptional( true ); //perhaps not quite per-spec, but a Good Thing anyway } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Join.java b/hibernate-core/src/main/java/org/hibernate/mapping/Join.java index 7f5eac7338..31aa638d98 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Join.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Join.java @@ -26,7 +26,6 @@ public class Join implements AttributeContainer, Serializable { private Table table; private KeyValue key; private PersistentClass persistentClass; - private boolean sequentialSelect; private boolean inverse; private boolean optional; private boolean disableForeignKeyCreation; @@ -176,13 +175,6 @@ public class Join implements AttributeContainer, Serializable { return deleteCheckStyle; } - public boolean isSequentialSelect() { - return sequentialSelect; - } - public void setSequentialSelect(boolean deferred) { - this.sequentialSelect = deferred; - } - public boolean isInverse() { return inverse; } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index 38d900c8e8..642cf96bf2 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -243,6 +243,7 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; +import org.hibernate.sql.results.graph.FetchParent; import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.embeddable.EmbeddableResultGraphNode; import org.hibernate.sql.results.graph.entity.internal.EntityResultImpl; @@ -611,9 +612,9 @@ public abstract class AbstractEntityPersister return deleteCallable[j]; } - protected boolean isSubclassTableSequentialSelect(int j) { - return false; - } +// protected boolean isSubclassTableSequentialSelect(int j) { +// return false; +// } /** * Decide which tables need to be updated. @@ -627,7 +628,6 @@ public abstract class AbstractEntityPersister * @return Array of booleans indicating which table require updating. */ public boolean[] getTableUpdateNeeded(final int[] dirtyProperties, boolean hasDirtyCollection) { - if ( dirtyProperties == null ) { return getTableHasColumns(); // for objects that came in via update() } @@ -697,31 +697,31 @@ public abstract class AbstractEntityPersister final NaturalIdDataAccess naturalIdRegionAccessStrategy, final RuntimeModelCreationContext creationContext) throws HibernateException { - this.factory = creationContext.getSessionFactory(); - this.sqlAliasStem = SqlAliasStemHelper.INSTANCE.generateStemFromEntityName( bootDescriptor.getEntityName() ); + factory = creationContext.getSessionFactory(); + sqlAliasStem = SqlAliasStemHelper.INSTANCE.generateStemFromEntityName( bootDescriptor.getEntityName() ); - this.navigableRole = new NavigableRole( bootDescriptor.getEntityName() ); + navigableRole = new NavigableRole( bootDescriptor.getEntityName() ); - SessionFactoryOptions sessionFactoryOptions = creationContext.getSessionFactory().getSessionFactoryOptions(); + final SessionFactoryOptions sessionFactoryOptions = creationContext.getSessionFactory().getSessionFactoryOptions(); if ( sessionFactoryOptions.isSecondLevelCacheEnabled() ) { - this.canWriteToCache = determineCanWriteToCache( bootDescriptor, cacheAccessStrategy ); - this.canReadFromCache = determineCanReadFromCache( bootDescriptor, cacheAccessStrategy ); this.cacheAccessStrategy = cacheAccessStrategy; - this.isLazyPropertiesCacheable = bootDescriptor.getRootClass().isLazyPropertiesCacheable(); this.naturalIdRegionAccessStrategy = naturalIdRegionAccessStrategy; + canWriteToCache = determineCanWriteToCache( bootDescriptor, cacheAccessStrategy ); + canReadFromCache = determineCanReadFromCache( bootDescriptor, cacheAccessStrategy ); + isLazyPropertiesCacheable = bootDescriptor.getRootClass().isLazyPropertiesCacheable(); } else { - this.canWriteToCache = false; - this.canReadFromCache = false; this.cacheAccessStrategy = null; - this.isLazyPropertiesCacheable = true; this.naturalIdRegionAccessStrategy = null; + canWriteToCache = false; + canReadFromCache = false; + isLazyPropertiesCacheable = true; } - this.entityMetamodel = new EntityMetamodel( bootDescriptor, this, creationContext ); + entityMetamodel = new EntityMetamodel( bootDescriptor, this, creationContext ); - this.entityEntryFactory = entityMetamodel.isMutable() + entityEntryFactory = entityMetamodel.isMutable() ? MutableEntityEntryFactory.INSTANCE : ImmutableEntityEntryFactory.INSTANCE; @@ -1855,74 +1855,12 @@ public abstract class AbstractEntityPersister @Override public String selectFragment(String alias, String suffix) { final QuerySpec rootQuerySpec = new QuerySpec( true ); - final String rootTableName = getRootTableName(); final LoaderSqlAstCreationState sqlAstCreationState = new LoaderSqlAstCreationState( rootQuerySpec, new SqlAliasBaseManager(), new SimpleFromClauseAccessImpl(), LockOptions.NONE, - (fetchParent, querySpec, creationState) -> { - final List fetches = new ArrayList<>(); - - fetchParent.getReferencedMappingContainer().visitFetchables( - fetchable -> { - // Ignore plural attributes - if ( fetchable instanceof PluralAttributeMapping ) { - return; - } - FetchTiming fetchTiming = fetchable.getMappedFetchOptions().getTiming(); - final boolean selectable; - if ( fetchable instanceof AttributeMapping ) { - if ( fetchParent instanceof EmbeddableResultGraphNode && ( (EmbeddableResultGraphNode) fetchParent ).getReferencedMappingContainer() == getIdentifierMapping() ) { - selectable = true; - } - else { - final int propertyNumber = ( (AttributeMapping) fetchable ).getStateArrayPosition(); - final int tableNumber = getSubclassPropertyTableNumber( propertyNumber ); - selectable = !isSubclassTableSequentialSelect( tableNumber ) - && propertySelectable[propertyNumber]; - } - } - else { - selectable = true; - } - if ( fetchable instanceof BasicValuedModelPart ) { - // Ignore lazy basic columns - if ( fetchTiming == FetchTiming.DELAYED ) { - return; - } - } - else if ( fetchable instanceof Association ) { - final Association association = (Association) fetchable; - // Ignore the fetchable if the FK is on the other side - if ( association.getSideNature() == ForeignKeyDescriptor.Nature.TARGET ) { - return; - } - // Ensure the FK comes from the root table - if ( !rootTableName.equals( association.getForeignKeyDescriptor().getKeyTable() ) ) { - return; - } - fetchTiming = FetchTiming.DELAYED; - } - - if ( selectable ) { - final NavigablePath navigablePath = fetchParent.resolveNavigablePath( fetchable ); - final Fetch fetch = fetchParent.generateFetchableFetch( - fetchable, - navigablePath, - fetchTiming, - true, - null, - creationState - ); - fetches.add( fetch ); - } - }, - null - ); - - return fetches; - }, + this::fetchProcessor, true, getFactory() ); @@ -2029,6 +1967,77 @@ public abstract class AbstractEntityPersister return expression; } + private List fetchProcessor(FetchParent fetchParent, QuerySpec querySpec, LoaderSqlAstCreationState creationState) { + final List fetches = new ArrayList<>(); + final String rootTableName = getRootTableName(); + fetchParent.getReferencedMappingContainer().visitFetchables( + fetchable -> { + // Ignore plural attributes + if ( !(fetchable instanceof PluralAttributeMapping ) ) { + final FetchTiming fetchTiming; + if ( fetchable instanceof BasicValuedModelPart ) { + // Ignore lazy basic columns + fetchTiming = fetchable.getMappedFetchOptions().getTiming(); + if ( fetchTiming == FetchTiming.DELAYED ) { + return; + } + } + else if ( fetchable instanceof Association ) { + final Association association = (Association) fetchable; + // Ignore the fetchable if the FK is on the other side + if ( association.getSideNature() == ForeignKeyDescriptor.Nature.TARGET ) { + return; + } + // Ensure the FK comes from the root table + if ( !rootTableName.equals( association.getForeignKeyDescriptor().getKeyTable() ) ) { + return; + } + fetchTiming = FetchTiming.DELAYED; + } + else { + fetchTiming = fetchable.getMappedFetchOptions().getTiming(); + } + + if ( fetchTiming == null ) { + throw new AssertionFailure("fetchTiming was null"); + } + + if ( isSelectable( fetchParent, fetchable ) ) { + final Fetch fetch = fetchParent.generateFetchableFetch( + fetchable, + fetchParent.resolveNavigablePath( fetchable ), + fetchTiming, + true, + null, + creationState + ); + fetches.add( fetch ); + } + } + }, + null + ); + return fetches; + } + + private boolean isSelectable(FetchParent fetchParent, Fetchable fetchable) { + if ( fetchable instanceof AttributeMapping ) { + if ( fetchParent instanceof EmbeddableResultGraphNode + && ( (EmbeddableResultGraphNode) fetchParent).getReferencedMappingContainer() == getIdentifierMapping() ) { + return true; + } + else { + final int propertyNumber = ( (AttributeMapping) fetchable).getStateArrayPosition(); +// final int tableNumber = getSubclassPropertyTableNumber( propertyNumber ); +// return !isSubclassTableSequentialSelect( tableNumber ) && propertySelectable[propertyNumber]; + return propertySelectable[propertyNumber]; + } + } + else { + return true; + } + } + @Override public String[] getIdentifierAliases(String suffix) { // NOTE: this assumes something about how propertySelectFragment is implemented by the subclass! @@ -4285,11 +4294,8 @@ public abstract class AbstractEntityPersister @Override public final void postInstantiate() throws MappingException { doLateInit(); - prepareLoader( singleIdEntityLoader ); prepareLoader( multiIdEntityLoader ); - - doPostInstantiate(); } private void prepareLoader(Loader loader) { @@ -4297,10 +4303,6 @@ public abstract class AbstractEntityPersister ( (Preparable) loader ).prepare(); } } - - protected void doPostInstantiate() { - } - /** * Load an instance using either the {@code forUpdateLoader} or the outer joining {@code loader}, * depending upon the value of the {@code lock} parameter @@ -4767,14 +4769,10 @@ public abstract class AbstractEntityPersister // for reattachment of mutable natural-ids, we absolutely positively have to grab the snapshot from the // database, because we have no other way to know if the state changed while detached. - final Object naturalIdSnapshot; final Object[] entitySnapshot = persistenceContext.getDatabaseSnapshot( id, this ); - if ( entitySnapshot == StatefulPersistenceContext.NO_ROW ) { - naturalIdSnapshot = null; - } - else { - naturalIdSnapshot = naturalIdMapping.extractNaturalIdFromEntityState( entitySnapshot, session ); - } + final Object naturalIdSnapshot = entitySnapshot == StatefulPersistenceContext.NO_ROW + ? null + : naturalIdMapping.extractNaturalIdFromEntityState(entitySnapshot, session); naturalIdResolutions.removeSharedResolution( id, naturalIdSnapshot, this ); naturalIdResolutions.manageLocalResolution( @@ -4787,17 +4785,11 @@ public abstract class AbstractEntityPersister @Override public Boolean isTransient(Object entity, SharedSessionContractImplementor session) throws HibernateException { - final Object id; - if ( canExtractIdOutOfEntity() ) { - id = getIdentifier( entity, session ); - } - else { - id = null; - } + final Object id = canExtractIdOutOfEntity() ? getIdentifier(entity, session) : null; // we *always* assume an instance with a null // identifier or no identifier property is unsaved! if ( id == null ) { - return Boolean.TRUE; + return true; } // check the version unsaved-value, if appropriate @@ -4823,7 +4815,7 @@ public abstract class AbstractEntityPersister final Object ck = cache.generateCacheKey( id, this, session.getFactory(), session.getTenantIdentifier() ); final Object ce = CacheHelper.fromSharedCache( session, ck, getCacheAccessStrategy() ); if ( ce != null ) { - return Boolean.FALSE; + return false; } } diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java index f94fabc126..ddb150015e 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java @@ -121,7 +121,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { // private final int[] subclassFormulaTableNumberClosure; private final String[] subclassColumnClosure; - private final boolean[] subclassTableSequentialSelect; +// private final boolean[] subclassTableSequentialSelect; // private final boolean[] subclassTableIsLazyClosure; private final boolean[] isInverseSubclassTable; private final boolean[] isNullableSubclassTable; @@ -311,7 +311,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { ArrayList subclassTableNames = new ArrayList<>(); ArrayList isConcretes = new ArrayList<>(); - ArrayList isDeferreds = new ArrayList<>(); +// ArrayList isDeferreds = new ArrayList<>(); // ArrayList isLazies = new ArrayList<>(); ArrayList isInverses = new ArrayList<>(); ArrayList isNullables = new ArrayList<>(); @@ -319,10 +319,10 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { keyColumns = new ArrayList<>(); for ( Table table : persistentClass.getSubclassTableClosure() ) { isConcretes.add( persistentClass.isClassOrSuperclassTable( table ) ); - isDeferreds.add( Boolean.FALSE ); -// isLazies.add( Boolean.FALSE ); - isInverses.add( Boolean.FALSE ); - isNullables.add( Boolean.FALSE ); +// isDeferreds.add( false ); +// isLazies.add( false ); + isInverses.add( false ); + isNullables.add( false ); final String tableName = determineTableName( table ); subclassTableNames.add( tableName ); String[] key = new String[idColumnSpan]; @@ -338,7 +338,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { final Table joinTable = join.getTable(); isConcretes.add( persistentClass.isClassOrSuperclassTable( joinTable ) ); - isDeferreds.add( join.isSequentialSelect() ); +// isDeferreds.add( join.isSequentialSelect() ); isInverses.add( join.isInverse() ); isNullables.add( join.isOptional() ); // isLazies.add( join.isLazy() ); @@ -356,7 +356,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { String[] naturalOrderSubclassTableNameClosure = ArrayHelper.toStringArray( subclassTableNames ); String[][] naturalOrderSubclassTableKeyColumnClosure = ArrayHelper.to2DStringArray( keyColumns ); isClassOrSuperclassTable = ArrayHelper.toBooleanArray( isConcretes ); - subclassTableSequentialSelect = ArrayHelper.toBooleanArray( isDeferreds ); +// subclassTableSequentialSelect = ArrayHelper.toBooleanArray( isDeferreds ); // subclassTableIsLazyClosure = ArrayHelper.toBooleanArray( isLazies ); isInverseSubclassTable = ArrayHelper.toBooleanArray( isInverses ); isNullableSubclassTable = ArrayHelper.toBooleanArray( isNullables ); @@ -729,10 +729,10 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { return isInverseTable[j]; } - @Override - protected boolean isSubclassTableSequentialSelect(int j) { - return subclassTableSequentialSelect[j] && !isClassOrSuperclassTable[j]; - } +// @Override +// protected boolean isSubclassTableSequentialSelect(int j) { +// return subclassTableSequentialSelect[j] && !isClassOrSuperclassTable[j]; +// } /*public void postInstantiate() throws MappingException { super.postInstantiate(); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java index 598c3d9690..a731be2aa2 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/SingleTableEntityPersister.java @@ -93,7 +93,7 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { // private final boolean[] subclassTableIsLazyClosure; private final boolean[] isInverseSubclassTable; private final boolean[] isNullableSubclassTable; - private final boolean[] subclassTableSequentialSelect; +// private final boolean[] subclassTableSequentialSelect; private final String[][] subclassTableKeyColumnClosure; private final boolean[] isClassOrSuperclassTable; private final boolean[] isClassOrSuperclassJoin; @@ -252,7 +252,7 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { ArrayList joinKeyColumns = new ArrayList<>(); ArrayList isConcretes = new ArrayList<>(); ArrayList isClassOrSuperclassJoins = new ArrayList<>(); - ArrayList isDeferreds = new ArrayList<>(); +// ArrayList isDeferreds = new ArrayList<>(); ArrayList isInverses = new ArrayList<>(); ArrayList isNullables = new ArrayList<>(); // ArrayList isLazies = new ArrayList<>(); @@ -260,7 +260,7 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { joinKeyColumns.add( getIdentifierColumnNames() ); isConcretes.add( Boolean.TRUE ); isClassOrSuperclassJoins.add( Boolean.TRUE ); - isDeferreds.add( Boolean.FALSE ); +// isDeferreds.add( Boolean.FALSE ); isInverses.add( Boolean.FALSE ); isNullables.add( Boolean.FALSE ); // isLazies.add( Boolean.FALSE ); @@ -271,8 +271,8 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { isNullables.add( join.isOptional() ); // isLazies.add( lazyAvailable && join.isLazy() ); - boolean isDeferred = join.isSequentialSelect() && !persistentClass.isClassOrSuperclassJoin( join ) ; - isDeferreds.add( isDeferred ); +// boolean isDeferred = join.isSequentialSelect() && !persistentClass.isClassOrSuperclassJoin( join ) ; +// isDeferreds.add( isDeferred ); String joinTableName = determineTableName( join.getTable() ); subclassTables.add( joinTableName ); @@ -286,7 +286,7 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { joinKeyColumns.add( keyCols ); } - subclassTableSequentialSelect = ArrayHelper.toBooleanArray( isDeferreds ); +// subclassTableSequentialSelect = ArrayHelper.toBooleanArray( isDeferreds ); subclassTableNameClosure = ArrayHelper.toStringArray( subclassTables ); // subclassTableIsLazyClosure = ArrayHelper.toBooleanArray( isLazies ); subclassTableKeyColumnClosure = ArrayHelper.to2DStringArray( joinKeyColumns ); @@ -568,10 +568,10 @@ public class SingleTableEntityPersister extends AbstractEntityPersister { return propertyTableNumbers[property] == j; } - @Override - protected boolean isSubclassTableSequentialSelect(int j) { - return subclassTableSequentialSelect[j] && !isClassOrSuperclassTable[j]; - } +// @Override +// protected boolean isSubclassTableSequentialSelect(int j) { +// return subclassTableSequentialSelect[j] && !isClassOrSuperclassTable[j]; +// } // Execute the SQL: diff --git a/hibernate-core/src/main/java/org/hibernate/resource/jdbc/spi/StatementInspector.java b/hibernate-core/src/main/java/org/hibernate/resource/jdbc/spi/StatementInspector.java index fd91566f86..d9d4013410 100644 --- a/hibernate-core/src/main/java/org/hibernate/resource/jdbc/spi/StatementInspector.java +++ b/hibernate-core/src/main/java/org/hibernate/resource/jdbc/spi/StatementInspector.java @@ -20,8 +20,9 @@ import java.io.Serializable; */ public interface StatementInspector extends Serializable { /** - * Inspect the given SQL, possibly returning a different SQL to be used instead. Note that returning {@code null} - * is interpreted as returning the same SQL as was passed. + * Inspect the given SQL, possibly returning a different SQL to be used instead. + * Note that returning {@code null} is interpreted as returning the same SQL as + * was passed. * * @param sql The SQL to inspect * diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index 40de968813..d7bd34d132 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -129,7 +129,7 @@ public class EntityMetamodel implements Serializable { private final boolean inherited; private final boolean hasSubclasses; private final Set subclassEntityNames; - private final Map,String> entityNameByInheritenceClassMap; + private final Map,String> entityNameByInheritanceClassMap; private final BytecodeEnhancementMetadata bytecodeEnhancementMetadata; @@ -439,7 +439,7 @@ public class EntityMetamodel implements Serializable { entityNameByInheritanceClassMapLocal.put( subclass.getMappedClass(), subclass.getEntityName() ); } } - entityNameByInheritenceClassMap = CollectionHelper.toSmallMap( entityNameByInheritanceClassMapLocal ); + entityNameByInheritanceClassMap = CollectionHelper.toSmallMap( entityNameByInheritanceClassMapLocal ); } private static GenerationStrategyPair buildGenerationStrategyPair( @@ -1002,7 +1002,7 @@ public class EntityMetamodel implements Serializable { * @return The mapped entity-name, or null if no such mapping was found. */ public String findEntityNameByEntityClass(Class inheritanceClass) { - return entityNameByInheritenceClassMap.get( inheritanceClass ); + return entityNameByInheritanceClassMap.get( inheritanceClass ); } @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/Record.java b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/Record.java new file mode 100644 index 0000000000..7232197b5b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/Record.java @@ -0,0 +1,26 @@ +package org.hibernate.orm.test.secondarytable; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.SequenceGenerator; +import jakarta.persistence.Table; +import org.hibernate.annotations.SecondaryRow; + +@Entity +@Table(name = "Overview") +@SecondaryTable(name = "Details") +@SecondaryTable(name = "Extras") +@SecondaryRow(table = "Details", optional = false) +@SecondaryRow(table = "Extras", optional = true) +@SequenceGenerator(name="RecordSeq", sequenceName = "RecordId", allocationSize = 1) +public class Record { + @Id @GeneratedValue(generator = "RecordSeq") long id; + String name; + @Column(table = "Details") String text; + @Column(table = "Details") boolean enabled; + @Column(table = "Extras", name="`comment`") String comment; +} + diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/SecondaryRowTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/SecondaryRowTest.java new file mode 100644 index 0000000000..9a1a18e37b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/SecondaryRowTest.java @@ -0,0 +1,100 @@ +package org.hibernate.orm.test.secondarytable; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel(annotatedClasses = {Record.class,SpecialRecord.class}) +@SessionFactory +public class SecondaryRowTest { + @Test + public void testSecondaryRow(SessionFactoryScope scope) { + int seq = scope.getSessionFactory().getJdbcServices().getDialect().getSequenceSupport().supportsSequences() + ? 1 : 0; + + Record record = new Record(); + record.enabled = true; + record.text = "Hello World!"; + scope.inTransaction(s -> s.persist(record)); + scope.getCollectingStatementInspector().assertExecutedCount(2+seq); + scope.getCollectingStatementInspector().clear(); + + Record record2 = new Record(); + record2.enabled = true; + record2.text = "Hello World!"; + record2.comment = "Goodbye"; + scope.inTransaction(s -> s.persist(record2)); + scope.getCollectingStatementInspector().assertExecutedCount(3+seq); + scope.getCollectingStatementInspector().clear(); + + SpecialRecord specialRecord = new SpecialRecord(); + specialRecord.enabled = true; + specialRecord.text = "Hello World!"; + specialRecord.validated = LocalDateTime.now(); + scope.inTransaction(s -> s.persist(specialRecord)); + scope.getCollectingStatementInspector().assertExecutedCount(3+seq); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> assertNotNull(s.find(Record.class, record.id))); + scope.getCollectingStatementInspector().assertExecutedCount(1); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> assertNotNull(s.find(Record.class, record2.id))); + scope.getCollectingStatementInspector().assertExecutedCount(1); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> assertNotNull(s.find(Record.class, specialRecord.id))); + scope.getCollectingStatementInspector().assertExecutedCount(1); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> assertEquals(3, s.createQuery("from Record").getResultList().size())); + scope.getCollectingStatementInspector().assertExecutedCount(1); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> assertEquals(1, s.createQuery("from SpecialRecord").getResultList().size())); + scope.getCollectingStatementInspector().assertExecutedCount(1); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> { + Record r = s.find(Record.class, record.id); + r.text = "new text"; + r.comment = "the comment"; + }); + scope.getCollectingStatementInspector().assertExecutedCount(3); + assertTrue( scope.getCollectingStatementInspector().getSqlQueries().get(1).startsWith("update ") ); + assertTrue( scope.getCollectingStatementInspector().getSqlQueries().get(2).startsWith("insert ") ); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> { + Record r = s.find(Record.class, record.id); + r.comment = "new comment"; + }); + scope.getCollectingStatementInspector().assertExecutedCount(2); + assertTrue( scope.getCollectingStatementInspector().getSqlQueries().get(1).startsWith("update ") ); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> { + Record r = s.find(Record.class, record2.id); + r.comment = null; + }); + scope.getCollectingStatementInspector().assertExecutedCount(2); + assertTrue( scope.getCollectingStatementInspector().getSqlQueries().get(1).startsWith("delete ") ); + scope.getCollectingStatementInspector().clear(); + + scope.inTransaction(s -> { + SpecialRecord r = s.find(SpecialRecord.class, specialRecord.id); + r.validated = null; + r.timestamp = System.currentTimeMillis(); + }); + scope.getCollectingStatementInspector().assertExecutedCount(2); + assertTrue( scope.getCollectingStatementInspector().getSqlQueries().get(1).startsWith("delete ") ); + scope.getCollectingStatementInspector().clear(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/SpecialRecord.java b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/SpecialRecord.java new file mode 100644 index 0000000000..ff5f538b1e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/SpecialRecord.java @@ -0,0 +1,19 @@ +package org.hibernate.orm.test.secondarytable; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.SecondaryTable; +import org.hibernate.annotations.SecondaryRow; + +import java.time.LocalDateTime; + +@Entity +@SecondaryTable(name = "Options") +@SecondaryTable(name = "`View`") +@SecondaryRow(table = "`View`", owned = false) +public class SpecialRecord extends Record { + @Column(table = "Options") + LocalDateTime validated; + @Column(table = "`View`", name="`timestamp`") + Long timestamp; +}