diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java index 006c4c7b14..1e863dd017 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLSqlAstTranslator.java @@ -65,8 +65,14 @@ public class PostgreSQLSqlAstTranslator extends Abstrac return getDialect().getVersion().isSameOrAfter( 9, 4 ); } + @Override + protected String getForUpdate() { + return getDialect().getVersion().isSameOrAfter( 9, 3 ) ? " for no key update" : " for update"; + } + @Override protected String getForShare(int timeoutMillis) { + // Note that `for key share` is inappropriate as that only means "prevent PK changes" return " for share"; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/LockReference.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/LockReference.java new file mode 100644 index 0000000000..b9d5257497 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/LockReference.java @@ -0,0 +1,61 @@ +/* + * 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 . + */ + +//$Id$ +package org.hibernate.orm.test.jpa.lock; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.LockModeType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedQuery; +import jakarta.persistence.QueryHint; +import jakarta.persistence.Version; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class LockReference { + private Integer id; + private String name; + private Lock lock; + + public LockReference() { + } + + public LockReference(Integer id, String name) { + this.id = id; + this.name = name; + } + + @Id + 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; + } + + @ManyToOne + public Lock getLock() { + return lock; + } + + public void setLock(Lock lock) { + this.lock = lock; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/LockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/LockTest.java index 0e9fdca796..ac73d5d7aa 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/LockTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/LockTest.java @@ -19,9 +19,12 @@ import org.hibernate.LockOptions; import org.hibernate.Session; import org.hibernate.TransactionException; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.HSQLDialect; +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SQLServerDialect; @@ -50,6 +53,7 @@ import static org.hibernate.jpa.SpecHints.HINT_SPEC_QUERY_TIMEOUT; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -1156,11 +1160,91 @@ public class LockTest extends BaseEntityManagerFunctionalTestCase { } } + @Test(timeout = 5 * 1000) //5 seconds + @TestForIssue( jiraKey = "HHH-13135" ) + @SkipForDialect(value = { + MySQLDialect.class, + MariaDBDialect.class + }, strictMatching = true, comment = "With InnoDB, a FK constraint check acquires a shared lock that isn't compatible with an exclusive lock") + @SkipForDialect(value = HSQLDialect.class, comment = "Seems like FK constraint checks are not compatible with exclusive locks") + @SkipForDialect(value = AbstractHANADialect.class, comment = "Seems like FK constraint checks are not compatible with exclusive locks") + public void testLockInsertFkTarget() { + Lock lock = new Lock(); + lock.setName( "name" ); + + doInJPA( this::entityManagerFactory, entityManager -> { + entityManager.persist( lock ); + } ); + + doInJPA( this::entityManagerFactory, _entityManager -> { + + Lock lock2 = _entityManager.find( Lock.class, lock.getId(), LockModeType.PESSIMISTIC_WRITE ); + assertEquals( "lock mode should be PESSIMISTIC_WRITE ", LockModeType.PESSIMISTIC_WRITE, _entityManager.getLockMode( lock2 ) ); + + doInJPA( this::entityManagerFactory, entityManager -> { + TransactionUtil.setJdbcTimeout( entityManager.unwrap( Session.class ) ); + LockReference ref = new LockReference( 1, "name" ); + ref.setLock( entityManager.getReference( Lock.class, lock.getId() ) ); + + // Check that we can insert a LockReference, referring to a Lock that is PESSIMISTIC_WRITE locked + entityManager.persist( ref ); + } ); + } ); + doInJPA( this::entityManagerFactory, entityManager -> { + LockReference lockReference = entityManager.find( LockReference.class, 1 ); + assertNotNull( lockReference ); + } ); + } + + @Test(timeout = 5 * 1000) //5 seconds + @TestForIssue( jiraKey = "HHH-13135" ) + @SkipForDialect(value = { + MySQLDialect.class, + MariaDBDialect.class + }, strictMatching = true, comment = "With InnoDB, a FK constraint check acquires a shared lock that isn't compatible with an exclusive lock") + @SkipForDialect(value = HSQLDialect.class, comment = "Seems like FK constraint checks are not compatible with exclusive locks") + @SkipForDialect(value = AbstractHANADialect.class, comment = "Seems like FK constraint checks are not compatible with exclusive locks") + public void testLockUpdateFkTarget() { + Lock lock1 = new Lock(); + lock1.setName( "l1" ); + Lock lock2 = new Lock(); + lock2.setName( "l2" ); + LockReference ref = new LockReference( 1, "name" ); + ref.setLock( lock1 ); + + doInJPA( this::entityManagerFactory, entityManager -> { + entityManager.persist( lock1 ); + entityManager.persist( lock2 ); + entityManager.persist( ref ); + } ); + + doInJPA( this::entityManagerFactory, _entityManager -> { + + Lock l1 = _entityManager.find( Lock.class, lock1.getId(), LockModeType.PESSIMISTIC_WRITE ); + assertEquals( "lock mode should be PESSIMISTIC_WRITE ", LockModeType.PESSIMISTIC_WRITE, _entityManager.getLockMode( l1 ) ); + Lock l2 = _entityManager.find( Lock.class, lock2.getId(), LockModeType.PESSIMISTIC_WRITE ); + assertEquals( "lock mode should be PESSIMISTIC_WRITE ", LockModeType.PESSIMISTIC_WRITE, _entityManager.getLockMode( l2 ) ); + + doInJPA( this::entityManagerFactory, entityManager -> { + TransactionUtil.setJdbcTimeout( entityManager.unwrap( Session.class ) ); + LockReference lockReference = entityManager.find( LockReference.class, ref.getId() ); + + // Check that we can update a LockReference, referring to a Lock that is PESSIMISTIC_WRITE locked + lockReference.setLock( entityManager.getReference( Lock.class, lock2.getId() ) ); + } ); + } ); + doInJPA( this::entityManagerFactory, entityManager -> { + LockReference lockReference = entityManager.find( LockReference.class, 1 ); + assertEquals( lock2.getId(), lockReference.getLock().getId() ); + } ); + } + @Override public Class[] getAnnotatedClasses() { return new Class[] { Lock.class, - UnversionedLock.class + UnversionedLock.class, + LockReference.class }; }