diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/Department.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/Department.java new file mode 100644 index 0000000000..03a31bd5e8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/Department.java @@ -0,0 +1,45 @@ +/* + * 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.locking.jpa; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Basic; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name="departments") +public class Department { + @Id + private Integer id; + @Basic + private String name; + + protected Department() { + // for Hibernate use + } + + public Department(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; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/Employee.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/Employee.java new file mode 100644 index 0000000000..661fc9c925 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/Employee.java @@ -0,0 +1,71 @@ +/* + * 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.locking.jpa; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Basic; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +/** + * @author Steve Ebersole + */ +@Entity +@Table(name = "employees") +public class Employee { + @Id + private Integer id; + @Basic + private String name; + @Basic + private float salary; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "dept_fk") + private Department department; + + protected Employee() { + // for Hibernate use + } + + public Employee(Integer id, String name, float salary, Department department) { + this.id = id; + this.name = name; + this.salary = salary; + this.department = department; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public float getSalary() { + return salary; + } + + public void setSalary(float salary) { + this.salary = salary; + } + + public Department getDepartment() { + return department; + } + + public void setDepartment(Department department) { + this.department = department; + } +} \ No newline at end of file diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/FollowOnLockingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/FollowOnLockingTest.java new file mode 100644 index 0000000000..30566a2a43 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/jpa/FollowOnLockingTest.java @@ -0,0 +1,110 @@ +/* + * 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.locking.jpa; + +import java.util.List; + +import org.hibernate.query.spi.QueryImplementor; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +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.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.LockModeType; +import jakarta.persistence.LockTimeoutException; +import jakarta.persistence.PessimisticLockException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * @author Steve Ebersole + */ +@DomainModel(annotatedClasses = { Employee.class, Department.class }) +@SessionFactory(useCollectingStatementInspector = true) +public class FollowOnLockingTest { + + @Test + public void testQueryLockingWithoutFollowOn(SessionFactoryScope scope) { + testQueryLocking( scope, false ); + } + @Test + public void testQueryLockingWithFollowOn(SessionFactoryScope scope) { + testQueryLocking( scope, true ); + } + + public void testQueryLocking(SessionFactoryScope scope, boolean followOnLocking) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + scope.inSession( (s) -> { + // After a transaction commit, the lock mode is set to NONE, which the TCK also does + scope.inTransaction( + s, + session -> { + final Department engineering = new Department( 1, "Engineering" ); + session.persist( engineering ); + + session.persist( new Employee( 1, "John", 9F, engineering ) ); + session.persist( new Employee( 2, "Mary", 10F, engineering ) ); + session.persist( new Employee( 3, "June", 11F, engineering ) ); + } + ); + + scope.inTransaction( + s, + session -> { + statementInspector.clear(); + + final QueryImplementor query = session.createQuery( + "select e from Employee e where e.salary > 10", + Employee.class + ); + if ( followOnLocking ) { + query.setFollowOnLocking( true ); + } + query.setLockMode( LockModeType.PESSIMISTIC_READ ); + final List employees = query.list(); + + assertThat( employees ).hasSize( 1 ); + final LockModeType appliedLockMode = session.getLockMode( employees.get( 0 ) ); + assertThat( appliedLockMode ).isIn( + LockModeType.PESSIMISTIC_READ, + LockModeType.PESSIMISTIC_WRITE + ); + + if ( followOnLocking ) { + statementInspector.assertExecutedCount( 2 ); + } + else { + statementInspector.assertExecutedCount( 1 ); + } + + try { + // with the initial txn still active (locks still held), try to update the row from another txn + scope.inTransaction( (session2) -> { + final Employee june = session2.find( Employee.class, 3 ); + june.setSalary( 90000F ); + } ); + fail( "Locked entity update was allowed" ); + } + catch (PessimisticLockException | LockTimeoutException expected) { + } + } + ); + } ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { + session.createMutationQuery( "delete Employee" ).executeUpdate(); + session.createMutationQuery( "delete Department" ).executeUpdate(); + } ); + } +} \ No newline at end of file