diff --git a/hibernate-core/src/main/java/org/hibernate/LockMode.java b/hibernate-core/src/main/java/org/hibernate/LockMode.java index 8b2087eea4a7..d442a23bf6c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/LockMode.java +++ b/hibernate-core/src/main/java/org/hibernate/LockMode.java @@ -39,7 +39,6 @@ * * @see Session#lock(Object, LockMode) * @see LockModeType - * @see LockOptions * @see org.hibernate.annotations.OptimisticLocking */ public enum LockMode implements FindOption, RefreshOption { @@ -119,7 +118,10 @@ public enum LockMode implements FindOption, RefreshOption { * lock mode, if the lock is successfully obtained, are the same * as {@link #PESSIMISTIC_WRITE}. If the lock is not immediately * available, an exception occurs. + * + * @deprecated Use {@linkplain Timeouts#NO_WAIT} instead. */ + @Deprecated UPGRADE_NOWAIT, /** @@ -129,7 +131,10 @@ public enum LockMode implements FindOption, RefreshOption { * as {@link #PESSIMISTIC_WRITE}. But if the lock is not * immediately available, no exception occurs, but the locked * row is not returned from the database. + * + * @deprecated Use {@linkplain Locking.LockedRows#SKIP} instead. */ + @Deprecated UPGRADE_SKIPLOCKED, /** @@ -143,6 +148,10 @@ public enum LockMode implements FindOption, RefreshOption { * lock mode is equivalent to {@link #PESSIMISTIC_WRITE}. * * @see LockModeType#PESSIMISTIC_READ + * @see jakarta.persistence.Timeout + * @see Locking.Scope + * @see Locking.FollowOn + * @see Locking.LockedRows */ PESSIMISTIC_READ, @@ -152,6 +161,10 @@ public enum LockMode implements FindOption, RefreshOption { * Obtained via a {@code select for update} statement. * * @see LockModeType#PESSIMISTIC_WRITE + * @see jakarta.persistence.Timeout + * @see Locking.Scope + * @see Locking.FollowOn + * @see Locking.LockedRows */ PESSIMISTIC_WRITE, @@ -163,6 +176,10 @@ public enum LockMode implements FindOption, RefreshOption { * Only legal for versioned entity types. * * @see LockModeType#PESSIMISTIC_FORCE_INCREMENT + * @see jakarta.persistence.Timeout + * @see Locking.Scope + * @see Locking.FollowOn + * @see Locking.LockedRows */ PESSIMISTIC_FORCE_INCREMENT; diff --git a/hibernate-core/src/main/java/org/hibernate/LockOptions.java b/hibernate-core/src/main/java/org/hibernate/LockOptions.java index 36926ebedbf0..9a619e1a4db8 100644 --- a/hibernate-core/src/main/java/org/hibernate/LockOptions.java +++ b/hibernate-core/src/main/java/org/hibernate/LockOptions.java @@ -8,14 +8,12 @@ import jakarta.persistence.Timeout; import java.io.Serializable; -import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Set; -import static jakarta.persistence.PessimisticLockScope.NORMAL; import static java.util.Collections.emptySet; import static java.util.Collections.unmodifiableSet; @@ -28,8 +26,8 @@ * are: * *

@@ -160,9 +158,9 @@ public class LockOptions implements Serializable { private final boolean immutable; private LockMode lockMode; private Timeout timeout; - private PessimisticLockScope pessimisticLockScope; - private Boolean followOnLocking; - + private Locking.LockedRows lockedRowHandling; + private Locking.Scope scope; + private Locking.FollowOn followOnHandling; private Map aliasSpecificLockModes; /** @@ -174,9 +172,10 @@ public class LockOptions implements Serializable { */ public LockOptions() { immutable = false; - lockMode = LockMode.NONE; - timeout = Timeouts.WAIT_FOREVER; - pessimisticLockScope = NORMAL; + this.lockMode = LockMode.NONE; + this.timeout = Timeouts.WAIT_FOREVER; + this.scope = Locking.Scope.ROOT_ONLY; + this.lockedRowHandling = Locking.LockedRows.WAIT; } /** @@ -190,8 +189,9 @@ public LockOptions() { public LockOptions(LockMode lockMode) { immutable = false; this.lockMode = lockMode; - timeout = Timeouts.WAIT_FOREVER; - pessimisticLockScope = NORMAL; + this.timeout = Timeouts.WAIT_FOREVER; + this.scope = Locking.Scope.ROOT_ONLY; + this.lockedRowHandling = Locking.LockedRows.WAIT; } /** @@ -205,7 +205,8 @@ public LockOptions(LockMode lockMode, Timeout timeout) { immutable = false; this.lockMode = lockMode; this.timeout = timeout; - pessimisticLockScope = NORMAL; + this.scope = Locking.Scope.ROOT_ONLY; + this.lockedRowHandling = Locking.LockedRows.WAIT; } /** @@ -220,7 +221,8 @@ public LockOptions(LockMode lockMode, Timeout timeout, PessimisticLockScope scop immutable = false; this.lockMode = lockMode; this.timeout = timeout; - this.pessimisticLockScope = scope; + this.scope = Locking.Scope.fromJpaScope( scope ); + this.lockedRowHandling = Locking.LockedRows.WAIT; } /** @@ -230,7 +232,7 @@ protected LockOptions(boolean immutable, LockMode lockMode) { this.immutable = immutable; this.lockMode = lockMode; timeout = Timeouts.WAIT_FOREVER; - pessimisticLockScope = NORMAL; + scope = Locking.Scope.ROOT_ONLY; } @@ -274,8 +276,8 @@ public LockOptions(LockMode lockMode, int timeout, PessimisticLockScope scope) { public boolean isEmpty() { return lockMode == LockMode.NONE && timeout == Timeouts.WAIT_FOREVER - && followOnLocking == null - && pessimisticLockScope == NORMAL + && followOnHandling == null + && scope == Locking.Scope.ROOT_ONLY && !hasAliasSpecificLockModes(); } @@ -304,16 +306,16 @@ public LockOptions setLockMode(LockMode lockMode) { } /** - * The timeout associated with {@code this} options, defining a maximum - * amount of time that the database should wait to obtain a pessimistic - * lock before returning an error to the client. + * The associated timeout, defining a maximum amount of time that + * the database should wait to obtain a pessimistic lock before + * returning an error to the client. */ public Timeout getTimeout() { return timeout; } /** - * Set the {@linkplain #getTimeout() timeout} associated with {@code this} options. + * Set the associated lock {@linkplain #getTimeout() timeout}. * * @return {@code this} for method chaining * @@ -328,102 +330,56 @@ public LockOptions setTimeout(Timeout timeout) { } /** - * The {@linkplain #getTimeout() timeout}, in milliseconds, associated - * with {@code this} options. - *

- * {@link #NO_WAIT}, {@link #WAIT_FOREVER}, or {@link #SKIP_LOCKED} - * represent 3 "magic" values. - * - * @return a timeout in milliseconds, {@link #NO_WAIT}, - * {@link #WAIT_FOREVER}, or {@link #SKIP_LOCKED} + * Get the associated lock scope. */ - public int getTimeOut() { - return getTimeout().milliseconds(); + public Locking.Scope getScope() { + return scope; } /** - * Set the {@linkplain #getTimeout() timeout}, in milliseconds, associated - * with {@code this} options. - *

- * {@link #NO_WAIT}, {@link #WAIT_FOREVER}, or {@link #SKIP_LOCKED} - * represent 3 "magic" values. - * - * @return {@code this} for method chaining - * - * @see #getTimeOut + * Set the lock scope. */ - public LockOptions setTimeOut(int timeout) { - return setTimeout( Timeouts.interpretMilliSeconds( timeout ) ); + public LockOptions setScope(Locking.Scope lockScope) { + if ( immutable ) { + throw new UnsupportedOperationException("immutable global instance of LockOptions"); + } + this.scope = lockScope; + return this; } /** - * The current lock scope: - *

- * - * @return the current {@link PessimisticLockScope} + * Get the associated handling for follow-on locking, if needed. */ - public PessimisticLockScope getLockScope() { - return pessimisticLockScope; + public Locking.FollowOn getFollowOn() { + return followOnHandling; } /** - * Set the lock scope: - * - * - * @param scope the new {@link PessimisticLockScope} - * @return {@code this} for method chaining + * Set the associated handling for follow-on locking, if needed. */ - public LockOptions setLockScope(PessimisticLockScope scope) { + public LockOptions setFollowOn(Locking.FollowOn followOnHandling) { if ( immutable ) { throw new UnsupportedOperationException("immutable global instance of LockOptions"); } - pessimisticLockScope = scope; + this.followOnHandling = followOnHandling; return this; } /** - * Returns a value indicating if follow-on locking was force - * enabled or disabled, overriding the default behavior of - * the SQL dialect. - * - * @return {@code true} if follow-on locking was force enabled, - * {@code false} if follow-on locking was force disabled, - * or {@code null} if the default behavior of the dialect - * has not been overridden. - * - * @see org.hibernate.jpa.HibernateHints#HINT_FOLLOW_ON_LOCKING - * @see org.hibernate.dialect.Dialect#useFollowOnLocking(String, org.hibernate.query.spi.QueryOptions) + * How locked rows should be handled. */ - public Boolean getFollowOnLocking() { - return followOnLocking; + public Locking.LockedRows getLockedRowHandling() { + return lockedRowHandling; } /** - * Force enable or disable the use of follow-on locking, - * overriding the default behavior of the SQL dialect. - * - * @param followOnLocking The new follow-on locking setting - * @return {@code this} for method chaining - * - * @see org.hibernate.jpa.HibernateHints#HINT_FOLLOW_ON_LOCKING - * @see org.hibernate.dialect.Dialect#useFollowOnLocking(String, org.hibernate.query.spi.QueryOptions) + * Specify how locked rows should be handled. */ - public LockOptions setFollowOnLocking(Boolean followOnLocking) { + public void setLockedRowHandling(Locking.LockedRows lockedRowHandling) { if ( immutable ) { throw new UnsupportedOperationException("immutable global instance of LockOptions"); } - this.followOnLocking = followOnLocking; - return this; + this.lockedRowHandling = lockedRowHandling; } /** @@ -458,13 +414,7 @@ public LockOptions makeDefensiveCopy() { * merging the alias-specific lock modes. */ public void overlay(LockOptions lockOptions) { - setLockMode( lockOptions.getLockMode() ); - setLockScope( lockOptions.getLockScope() ); - setTimeOut( lockOptions.getTimeOut() ); - if ( lockOptions.aliasSpecificLockModes != null ) { - lockOptions.aliasSpecificLockModes.forEach(this::setAliasSpecificLockMode); - } - setFollowOnLocking( lockOptions.getFollowOnLocking() ); + copy( lockOptions, this ); } /** @@ -478,12 +428,13 @@ public void overlay(LockOptions lockOptions) { */ public static LockOptions copy(LockOptions source, LockOptions destination) { destination.setLockMode( source.getLockMode() ); - destination.setLockScope( source.getLockScope() ); - destination.setTimeOut( source.getTimeOut() ); + destination.setScope( source.getScope() ); + destination.setTimeout( source.getTimeout() ); + destination.setFollowOn( source.getFollowOn() ); + destination.setLockedRowHandling( source.getLockedRowHandling() ); if ( source.aliasSpecificLockModes != null ) { - destination.aliasSpecificLockModes = new HashMap<>( source.aliasSpecificLockModes ); + source.aliasSpecificLockModes.forEach(destination::setAliasSpecificLockMode); } - destination.setFollowOnLocking( source.getFollowOnLocking() ); return destination; } @@ -497,16 +448,135 @@ else if ( !(object instanceof LockOptions that) ) { } else { return timeout == that.timeout - && pessimisticLockScope == that.pessimisticLockScope + && scope == that.scope && lockMode == that.lockMode && Objects.equals( aliasSpecificLockModes, that.aliasSpecificLockModes ) - && Objects.equals( followOnLocking, that.followOnLocking ); + && Objects.equals( followOnHandling, that.followOnHandling ); } } @Override public int hashCode() { - return Objects.hash( lockMode, timeout, aliasSpecificLockModes, followOnLocking, pessimisticLockScope ); + return Objects.hash( + lockMode, + timeout, + scope, + lockedRowHandling, + followOnHandling, + aliasSpecificLockModes + ); + } + + /** + * The {@linkplain #getTimeout() timeout}, in milliseconds, associated + * with {@code this} options. + *

+ * {@link #NO_WAIT}, {@link #WAIT_FOREVER}, or {@link #SKIP_LOCKED} + * represent 3 "magic" values. + * + * @return a timeout in milliseconds, {@link #NO_WAIT}, + * {@link #WAIT_FOREVER}, or {@link #SKIP_LOCKED} + */ + public int getTimeOut() { + return getTimeout().milliseconds(); + } + + /** + * Set the {@linkplain #getTimeout() timeout}, in milliseconds, associated + * with {@code this} options. + *

+ * {@link #NO_WAIT}, {@link #WAIT_FOREVER}, or {@link #SKIP_LOCKED} + * represent 3 "magic" values. + * + * @return {@code this} for method chaining + * + * @see #getTimeOut + */ + public LockOptions setTimeOut(int timeout) { + return setTimeout( Timeouts.interpretMilliSeconds( timeout ) ); + } + + /** + * Returns a value indicating if follow-on locking was force + * enabled or disabled, overriding the default behavior of + * the SQL dialect. + * + * @return {@code true} if follow-on locking was force enabled, + * {@code false} if follow-on locking was force disabled, + * or {@code null} if the default behavior of the dialect + * has not been overridden. + * + * @see org.hibernate.jpa.HibernateHints#HINT_FOLLOW_ON_LOCKING + * @see org.hibernate.dialect.Dialect#useFollowOnLocking(String, org.hibernate.query.spi.QueryOptions) + * + * @deprecated Use {@link #getFollowOn()} instead. + */ + @Deprecated(since = "7.1") + public Boolean getFollowOnLocking() { + return followOnHandling == Locking.FollowOn.ALLOW; + } + + /** + * Force enable or disable the use of follow-on locking, + * overriding the default behavior of the SQL dialect. + * + * @param followOnLocking The new follow-on locking setting + * @return {@code this} for method chaining + * + * @see org.hibernate.jpa.HibernateHints#HINT_FOLLOW_ON_LOCKING + * @see org.hibernate.dialect.Dialect#useFollowOnLocking(String, org.hibernate.query.spi.QueryOptions) + * + * @deprecated Use {@link #setFollowOn} instead. + */ + @Deprecated(since = "7.1") + public LockOptions setFollowOnLocking(Boolean followOnLocking) { + if ( immutable ) { + throw new UnsupportedOperationException("immutable global instance of LockOptions"); + } + if ( followOnLocking == Boolean.FALSE ) { + followOnHandling = Locking.FollowOn.DISALLOW; + } + else { + followOnHandling = Locking.FollowOn.ALLOW; + } + return this; + } + + /** + * The current lock scope: + *

+ * + * @return the current {@link PessimisticLockScope} + * + * @deprecated Use {@link #getScope()} instead + */ + @Deprecated(since = "7.1") + public PessimisticLockScope getLockScope() { + return scope.getCorrespondingJpaScope(); + } + + /** + * Set the lock scope: + * + * + * @param scope the new {@link PessimisticLockScope} + * @return {@code this} for method chaining + * + * @deprecated Use {@link #setScope} instead + */ + @Deprecated(since = "7.1") + public LockOptions setLockScope(PessimisticLockScope scope) { + return setScope( Locking.Scope.fromJpaScope( scope ) ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/Locking.java b/hibernate-core/src/main/java/org/hibernate/Locking.java new file mode 100644 index 000000000000..a50d8bd40734 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/Locking.java @@ -0,0 +1,136 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate; + +import jakarta.persistence.FindOption; +import jakarta.persistence.LockOption; +import jakarta.persistence.PessimisticLockScope; +import jakarta.persistence.RefreshOption; +import jakarta.persistence.Timeout; +import org.hibernate.dialect.Dialect; + +/** + * Support for various aspects of pessimistic locking. + * + * @see LockMode#PESSIMISTIC_READ + * @see LockMode#PESSIMISTIC_WRITE + * @see LockMode#PESSIMISTIC_FORCE_INCREMENT + * + * @author Steve Ebersole + */ +public interface Locking { + + /** + * When pessimistic locking is requested, this enum defines + * what exactly will be locked. + * + * @apiNote Same intention as the JPA {@linkplain PessimisticLockScope}, + * but offering the additional {@linkplain #INCLUDE_FETCHES} behavior. + * + * @see FollowOn + */ + enum Scope implements FindOption, LockOption, RefreshOption { + /** + * Lock the database row(s) that correspond to the non-collection-valued + * persistent state of that instance. If a joined inheritance strategy is + * used, or if the entity is otherwise mapped to a secondary table, this + * entails locking the row(s) for the entity instance in the additional table(s). + * + * @see PessimisticLockScope#NORMAL + */ + ROOT_ONLY( PessimisticLockScope.NORMAL ), + + /** + * In addition to the locking behavior specified for {@linkplain #ROOT_ONLY}, + * rows for collection tables ({@linkplain jakarta.persistence.ElementCollection}, + * {@linkplain jakarta.persistence.OneToMany} and {@linkplain jakarta.persistence.ManyToMany}) + * will also be locked. + * + * @see PessimisticLockScope#EXTENDED + */ + INCLUDE_COLLECTIONS( PessimisticLockScope.EXTENDED ), + + /** + * All tables with fetched rows will be locked. + * + * @apiNote This is Hibernate's legacy behavior, and has no + * corresponding JPA scope. + */ + INCLUDE_FETCHES( null ); + + private final PessimisticLockScope jpaScope; + + Scope(PessimisticLockScope jpaScope) { + this.jpaScope = jpaScope; + } + + /** + * The JPA PessimisticLockScope which corresponds to this LockScope. + * + * @return The corresponding PessimisticLockScope, or {@code null}. + */ + public PessimisticLockScope getCorrespondingJpaScope() { + return jpaScope; + } + + public static Scope fromJpaScope(PessimisticLockScope scope) { + if ( scope == PessimisticLockScope.EXTENDED ) { + return INCLUDE_COLLECTIONS; + } + // null, NORMAL + return ROOT_ONLY; + } + } + + /** + * In certain circumstances, Hibernate may need to acquire locks + * through the use of subsequent queries. For example, some + * databases may not like attempting to lock rows in a join or + * queries with paging. In such cases, Hibernate will fall back + * to issuing additional lock queries to lock some of the rows. + * This option controls whether Hibernate is allowed to use + * this approach. + */ + enum FollowOn implements FindOption, LockOption, RefreshOption { + /** + * Allow Hibernate to perform follow-on locking when it needs to. + */ + ALLOW, + + /** + * Do not allow Hibernate to perform follow-on locking when it needs to, throwing + * an exception instead. + */ + DISALLOW, + + /** + * Do not allow Hibernate to perform follow-on locking when it needs to, but just ignore + * the situation. + * + * @apiNote This can lead to rows not being locked when they are expected to be. + */ + IGNORE + } + + /** + * How locked rows should be handled with pessimistic lock attempts. + * The default is to {@linkplain #WAIT wait} for the locks to be released. + */ + enum LockedRows implements FindOption, LockOption, RefreshOption { + /** + * The default. The transaction will wait for the row locks to + * be released, within any specified {@linkplain Timeout timeout}. + */ + WAIT, + + /** + * Immediately skips locked rows. + * + * @apiNote Only legal if the database + * {@linkplain Dialect#supportsSkipLocked() supports skipping locked rows}. + */ + SKIP + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index d140524d8bd0..ace166339423 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -22,11 +22,13 @@ import java.util.function.Function; import java.util.function.Supplier; +import jakarta.persistence.Timeout; import org.hibernate.AssertionFailure; import org.hibernate.Internal; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.QueryException; +import org.hibernate.Timeouts; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.RowLockStrategy; @@ -1756,7 +1758,7 @@ else if ( forUpdate != null ) { } protected void renderForUpdateClause(QuerySpec querySpec, ForUpdateClause forUpdateClause) { - int timeoutMillis = forUpdateClause.getTimeoutMillis(); + Timeout timeout = forUpdateClause.getTimeout(); LockKind lockKind = LockKind.NONE; switch ( forUpdateClause.getLockMode() ) { case PESSIMISTIC_WRITE: @@ -1767,11 +1769,11 @@ protected void renderForUpdateClause(QuerySpec querySpec, ForUpdateClause forUpd break; case UPGRADE_NOWAIT: case PESSIMISTIC_FORCE_INCREMENT: - timeoutMillis = LockOptions.NO_WAIT; + timeout = Timeouts.NO_WAIT; lockKind = LockKind.UPDATE; break; case UPGRADE_SKIPLOCKED: - timeoutMillis = LockOptions.SKIP_LOCKED; + timeout = Timeouts.SKIP_LOCKED; lockKind = LockKind.UPDATE; break; default: @@ -1779,7 +1781,7 @@ protected void renderForUpdateClause(QuerySpec querySpec, ForUpdateClause forUpd } if ( lockKind != LockKind.NONE ) { if ( lockKind == LockKind.SHARE ) { - appendSql( getForShare( timeoutMillis ) ); + appendSql( getForShare( timeout.milliseconds() ) ); if ( forUpdateClause.hasAliases() && dialect.getReadRowLockStrategy() != RowLockStrategy.NONE ) { appendSql( " of " ); forUpdateClause.appendAliases( this ); @@ -1793,23 +1795,23 @@ protected void renderForUpdateClause(QuerySpec querySpec, ForUpdateClause forUpd } } appendSql( getForUpdateWithClause() ); - switch ( timeoutMillis ) { - case LockOptions.NO_WAIT: + switch ( timeout.milliseconds() ) { + case Timeouts.NO_WAIT_MILLI: if ( dialect.supportsNoWait() ) { appendSql( getNoWait() ); } break; - case LockOptions.SKIP_LOCKED: + case Timeouts.SKIP_LOCKED_MILLI: if ( dialect.supportsSkipLocked() ) { appendSql( getSkipLocked() ); } break; - case LockOptions.WAIT_FOREVER: + case Timeouts.WAIT_FOREVER_MILLI: break; default: if ( dialect.supportsWait() ) { appendSql( " wait " ); - appendSql( Math.round( timeoutMillis / 1e3f ) ); + appendSql( Timeouts.getTimeoutInSeconds( timeout ) ); } break; } @@ -1843,24 +1845,28 @@ protected String getSkipLocked() { return " skip locked"; } - protected LockMode getEffectiveLockMode(String alias) { - final QueryPart currentQueryPart = getQueryPartStack().getCurrent(); - return currentQueryPart == null - ? LockMode.NONE - : getEffectiveLockMode( alias, currentQueryPart.isRoot() ); - } - - protected LockMode getEffectiveLockMode(String alias, boolean isRoot) { + protected LockMode getEffectiveLockMode() { if ( getLockOptions() == null ) { return LockMode.NONE; } - LockMode lockMode = getLockOptions().getAliasSpecificLockMode( alias ); - if ( isRoot && lockMode == null ) { - lockMode = getLockOptions().getLockMode(); + + final QueryPart currentQueryPart = getQueryPartStack().getCurrent(); + if ( currentQueryPart == null ) { + return LockMode.NONE; } + + final LockMode lockMode = getLockOptions().getLockMode(); return lockMode == null ? LockMode.NONE : lockMode; } + protected LockMode getEffectiveLockMode(String alias) { + return getEffectiveLockMode(); + } + + protected LockMode getEffectiveLockMode(String alias, boolean isRoot) { + return getEffectiveLockMode(); + } + protected int getEffectiveLockTimeout(LockMode lockMode) { return getLockOptions() == null ? LockOptions.WAIT_FOREVER @@ -5878,7 +5884,9 @@ protected void renderRootTableGroup(TableGroup tableGroup, List registerAffectedTable( querySpaces[i] ); } } - if ( !usesLockHint && tableGroup.getSourceAlias() != null && LockMode.READ.lessThan( effectiveLockMode ) ) { + if ( !usesLockHint + && tableGroup.getSourceAlias() != null + && LockMode.READ.lessThan( effectiveLockMode ) ) { if ( forUpdate == null ) { forUpdate = new ForUpdateClause( effectiveLockMode ); } @@ -8421,7 +8429,7 @@ protected enum LockStrategy { protected static class ForUpdateClause { private LockMode lockMode; - private int timeoutMillis = LockOptions.WAIT_FOREVER; + private Timeout timeout = Timeouts.WAIT_FOREVER; private Map keyColumnNames; private Map aliases; @@ -8430,21 +8438,21 @@ public ForUpdateClause(LockMode lockMode) { } public ForUpdateClause() { - this.lockMode = LockMode.NONE; + this( LockMode.NONE ); } - public void applyAliases(RowLockStrategy lockIdentifier, QuerySpec querySpec) { - if ( lockIdentifier != RowLockStrategy.NONE ) { - querySpec.getFromClause().visitTableGroups( tableGroup -> applyAliases( lockIdentifier, tableGroup ) ); + public void applyAliases(RowLockStrategy rowLockStrategy, QuerySpec querySpec) { + if ( rowLockStrategy != RowLockStrategy.NONE ) { + querySpec.getFromClause().visitTableGroups( (tableGroup) -> applyAliases( rowLockStrategy, tableGroup ) ); } } - public void applyAliases(RowLockStrategy lockIdentifier, TableGroup tableGroup) { - if ( aliases != null && lockIdentifier != RowLockStrategy.NONE ) { + public void applyAliases(RowLockStrategy rowLockStrategy, TableGroup tableGroup) { + if ( aliases != null && rowLockStrategy != RowLockStrategy.NONE ) { final String tableAlias = tableGroup.getPrimaryTableReference().getIdentificationVariable(); if ( aliases.containsKey( tableGroup.getSourceAlias() ) ) { addAlias( tableGroup.getSourceAlias(), tableAlias ); - if ( lockIdentifier == RowLockStrategy.COLUMN ) { + if ( rowLockStrategy == RowLockStrategy.COLUMN ) { addKeyColumnNames( tableGroup ); } } @@ -8507,8 +8515,12 @@ private void addAlias(String alias, String tableAlias) { aliases.put( alias, tableAlias ); } + public Timeout getTimeout() { + return timeout; + } + public int getTimeoutMillis() { - return timeoutMillis; + return getTimeout().milliseconds(); } public boolean hasAliases() { @@ -8579,7 +8591,7 @@ public void merge(LockOptions lockOptions) { } } lockMode = upgradeType; - timeoutMillis = lockOptions.getTimeOut(); + timeout = lockOptions.getTimeout(); } } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Book.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Book.java new file mode 100644 index 000000000000..218869a53474 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Book.java @@ -0,0 +1,92 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.locking.scope; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; + +import java.util.HashSet; +import java.util.Set; + +/** + * @author Steve Ebersole + */ +@Entity +public class Book { + @Id + private Integer id; + private String title; + private String synopsis; + @ManyToOne + @JoinColumn(name = "publisher_fk") + private Publisher publisher; + @ManyToMany + @JoinTable(name = "book_authors", + joinColumns = @JoinColumn(name = "book_fk"), + inverseJoinColumns = @JoinColumn(name = "author_fk")) + private Set authors; + + protected Book() { + // for Hibernate use + } + + public Book(Integer id, String title, String synopsis) { + this( id, title, synopsis, null ); + } + + public Book(Integer id, String title, String synopsis, Publisher publisher) { + this.id = id; + this.title = title; + this.synopsis = synopsis; + this.publisher = publisher; + } + + public Integer getId() { + return id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getSynopsis() { + return synopsis; + } + + public void setSynopsis(String synopsis) { + this.synopsis = synopsis; + } + + public Publisher getPublisher() { + return publisher; + } + + public void setPublisher(Publisher publisher) { + this.publisher = publisher; + } + + public Set getAuthors() { + return authors; + } + + public void setAuthors(Set authors) { + this.authors = authors; + } + + public void addAuthor(Person author) { + if ( authors == null ) { + authors = new HashSet<>(); + } + authors.add( author ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Person.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Person.java new file mode 100644 index 000000000000..030bad910263 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Person.java @@ -0,0 +1,41 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.locking.scope; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * @author Steve Ebersole + */ +@Entity +public class Person { + @Id + private Integer id; + @Basic + private String name; + + protected Person() { + // for Hibernate use + } + + public Person(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Publisher.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Publisher.java new file mode 100644 index 000000000000..f3bbdcf58162 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/Publisher.java @@ -0,0 +1,71 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.locking.scope; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import java.util.Set; + +/** + * @author Steve Ebersole + */ +@Entity +public class Publisher { + @Id + private Integer id; + private String name; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="editor_fk") + private Person leadEditor; + @OneToMany(mappedBy = "publisher") + private Set books; + + protected Publisher() { + // for Hibernate use + } + + public Publisher(Integer id, String name) { + this( id, name, null ); + } + + public Publisher(Integer id, String name, Person leadEditor) { + this.id = id; + this.name = name; + this.leadEditor = leadEditor; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Person getLeadEditor() { + return leadEditor; + } + + public void setLeadEditor(Person leadEditor) { + this.leadEditor = leadEditor; + } + + public Set getBooks() { + return books; + } + + public void setBooks(Set books) { + this.books = books; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/SimpleScopingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/SimpleScopingTests.java new file mode 100644 index 000000000000..1667417ae7f3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/scope/SimpleScopingTests.java @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.locking.scope; + +import jakarta.persistence.LockModeType; +import org.hibernate.Hibernate; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.NotImplementedYet; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@DomainModel(annotatedClasses = {Book.class, Person.class, Publisher.class}) +@SessionFactory(useCollectingStatementInspector = true) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19336" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19459" ) +public class SimpleScopingTests { + @BeforeEach + void createTestData(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + final Person milton = new Person( 1, "John Milton" ); + session.persist( milton ); + final Person campbell = new Person( 2, "Joseph Campbell" ); + session.persist( campbell ); + final Person king = new Person( 3, "Stephen King" ); + session.persist( king ); + final Person straub = new Person( 4, "Peter Straub" ); + session.persist( straub ); + final Person doe = new Person( 5, "John Doe" ); + session.persist( doe ); + + final Publisher acme = new Publisher( 1, "Acme Publishing House", doe ); + session.persist( acme ); + + final Book paradiseLost = new Book( 1, "Paradise Lost", "Narrative poem, in the epic style, ..." ); + paradiseLost.addAuthor( milton ); + session.persist( paradiseLost ); + + final Book thePowerOfMyth = new Book( 2, "The Power of Myth", + "A look at the themes and symbols of ancient narratives ..." ); + thePowerOfMyth.addAuthor( campbell ); + session.persist( thePowerOfMyth ); + + final Book theTalisman = new Book( 3, "The Talisman", "Epic of the struggle between good and evil ...", acme ); + theTalisman.addAuthor( king ); + theTalisman.addAuthor( straub ); + session.persist( theTalisman ); + + final Book theDarkTower = new Book( 4, "The Dark Tower", "The epic final to the series ...", acme ); + theDarkTower.addAuthor( king ); + session.persist( theDarkTower ); + } ); + } + + @AfterEach + void dropTestData(SessionFactoryScope factoryScope) { + factoryScope.dropData(); + } + + @Test + void testLoading(SessionFactoryScope factoryScope) { + factoryScope.inTransaction( (session) -> { + final Book paradiseLost = session.find( Book.class, 1 ); + assertThat( paradiseLost.getTitle() ).isEqualTo( "Paradise Lost" ); + assertThat( Hibernate.isInitialized( paradiseLost.getAuthors() ) ).isFalse(); + + final Book talisman = session.find( Book.class, 3 ); + assertThat( talisman.getTitle() ).isEqualTo( "The Talisman" ); + assertThat( Hibernate.isInitialized( talisman.getAuthors() ) ).isFalse(); + } ); + } + + @Test + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsTableLocking.class ) + @NotImplementedYet(reason = "Proper, strict-JPA locking not yet implemented", expectedVersion = "7.1") + void testSimpleLocking(SessionFactoryScope factoryScope) { + final SQLStatementInspector sqlCollector = factoryScope.getCollectingStatementInspector(); + + factoryScope.inTransaction( (session) -> { + sqlCollector.clear(); + final Book theTalisman = session.find( Book.class, 3, LockModeType.PESSIMISTIC_WRITE ); + assertThat( Hibernate.isInitialized( theTalisman ) ).isTrue(); + assertThat( Hibernate.isInitialized( theTalisman.getPublisher() ) ).isTrue(); + assertThat( Hibernate.isInitialized( theTalisman.getAuthors() ) ).isFalse(); + assertThat( Hibernate.isInitialized( theTalisman.getPublisher().getLeadEditor() ) ).isFalse(); + assertThat( Hibernate.isInitialized( theTalisman.getPublisher().getBooks() ) ).isFalse(); + assertThat( sqlCollector.getSqlQueries() ).hasSize( 1 ); + + // todo: this is going to depend on the specific database + assertThat( sqlCollector.getSqlQueries().get( 0 ) ).contains( " for update of " ); + } ); + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index ea7a8b5a0f7a..afc560027915 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -55,6 +55,7 @@ import org.hibernate.dialect.NationalizationSupport; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.RowLockStrategy; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SpannerDialect; import org.hibernate.dialect.SybaseASEDialect; @@ -265,6 +266,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsTableLocking implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect.getReadRowLockStrategy() != RowLockStrategy.NONE; + } + } + public static class DoubleQuoteQuoting implements DialectFeatureCheck { @Override public boolean apply(Dialect dialect) {