From 4b16dede41ffe8a322f08cf293afd1c81e4e297e Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Tue, 23 Apr 2024 14:11:06 +0200 Subject: [PATCH 1/2] [#1891] Fix getReference issue with BE ORM 6.5 doesn't create a proxy factory unless is needed. When we upgraded we missed a small change to the flow because we didn't have tests for it (`getReference` with Bytecode Enhancements enabled). --- .../reactive/event/impl/DefaultReactiveLoadEventListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java index e84387950..8f821a986 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java @@ -361,7 +361,7 @@ else if ( persister.getRepresentationStrategy().getProxyFactory() != null ) { // we have a HibernateProxy factory, this case is more complicated return loadWithProxyFactory( event, persister, keyToLoad ); } - else if ( !persister.hasSubclasses() ) { + else if ( persister.hasSubclasses() ) { // the entity class has subclasses and there is no HibernateProxy factory return load( event, persister, keyToLoad, options ); } From b12770774882f9aaac76aadf0cf1e12e141e3386 Mon Sep 17 00:00:00 2001 From: Davide D'Alto Date: Tue, 23 Apr 2024 11:38:46 +0200 Subject: [PATCH 2/2] [#1891] Add test case for getReference with BE Test `session.getReference` with Bytecode Enhancements enabled --- .../reactive/it/reference/Author.java | 77 +++++++ .../hibernate/reactive/it/reference/Book.java | 89 ++++++++ .../reactive/it/ReferenceBETest.java | 215 ++++++++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/reference/Author.java create mode 100644 integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/reference/Book.java create mode 100644 integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/ReferenceBETest.java diff --git a/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/reference/Author.java b/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/reference/Author.java new file mode 100644 index 000000000..65288aa0e --- /dev/null +++ b/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/reference/Author.java @@ -0,0 +1,77 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it.reference; + +import java.util.Objects; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity(name = "Writer") +@Table(name = "TAuthor") +public class Author { + + @Id + @GeneratedValue + private Integer id; + private String name; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private Book book; + + public Author() { + } + + public Author(String name, Book book) { + this.name = name; + this.book = book; + } + + 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; + } + + public Book getBook() { + return book; + } + + public void setBook(Book book) { + this.book = book; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Author author = (Author) o; + return Objects.equals( name, author.name ); + } + + @Override + public int hashCode() { + return Objects.hash( name ); + } +} diff --git a/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/reference/Book.java b/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/reference/Book.java new file mode 100644 index 000000000..c860584b1 --- /dev/null +++ b/integration-tests/bytecode-enhancements-it/src/main/java/org/hibernate/reactive/it/reference/Book.java @@ -0,0 +1,89 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it.reference; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import jakarta.persistence.Version; + +@Entity(name = "Tome") +@Table(name = "TBook") +public class Book { + + @Id + @GeneratedValue + private Integer id; + @Version + private Integer version = 1; + private String title; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "book") + private List authors = new ArrayList<>(); + + public Book() { + } + + public Book(String title) { + this.title = title; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Book book = (Book) o; + return Objects.equals( title, book.title ); + } + + @Override + public int hashCode() { + return Objects.hash( title ); + } +} diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/ReferenceBETest.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/ReferenceBETest.java new file mode 100644 index 000000000..034b118e7 --- /dev/null +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/ReferenceBETest.java @@ -0,0 +1,215 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import org.hibernate.Hibernate; +import org.hibernate.LockMode; +import org.hibernate.reactive.it.reference.Author; +import org.hibernate.reactive.it.reference.Book; +import org.hibernate.reactive.stage.Stage; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.loop; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Timeout(value = 10, timeUnit = MINUTES) + +public class ReferenceBETest extends BaseReactiveIT { + + @Override + protected Collection> annotatedEntities() { + return List.of( Author.class, Book.class ); + } + + @Override + public CompletionStage deleteEntities(Class... entities) { + return getSessionFactory() + .withTransaction( s -> loop( entities, entityClass -> s + .createQuery( "from " + entityName( entityClass ), entityClass ) + .getResultList() + .thenCompose( list -> loop( list, s::remove ) ) ) ); + } + + private String entityName(Class entityClass) { + if ( Author.class.equals( entityClass ) ) { + return "Writer"; + } + return "Tome"; + } + + @Test + public void testDetachedEntityReference(VertxTestContext context) { + final Book goodOmens = new Book( "Good Omens: The Nice and Accurate Prophecies of Agnes Nutter, Witch" ); + + test( context, getSessionFactory() + .withSession( s -> s.persist( goodOmens ).thenCompose( v -> s.flush() ) ) + .thenCompose( v -> getSessionFactory().withSession( s -> { + Book book = s.getReference( Book.class, goodOmens.getId() ); + assertFalse( Hibernate.isInitialized( book ) ); + return s.persist( new Author( "Neil Gaiman", book ) ) + .thenCompose( vv -> s.flush() ); + } ) ) + .thenCompose( v -> getSessionFactory().withSession( s -> { + Book book = s.getReference( goodOmens ); + assertFalse( Hibernate.isInitialized( book ) ); + return s.persist( new Author( "Terry Pratchett", book ) ) + .thenCompose( vv -> s.flush() ); + } ) ) + .thenCompose( v -> getSessionFactory().withSession( s -> { + Book book = s.getReference( goodOmens ); + assertFalse( Hibernate.isInitialized( book ) ); + return Stage.fetch( book ).thenCompose( vv -> Stage.fetch( book.getAuthors() ) ); + } ) ) + .thenAccept( optionalAssociation -> { + assertTrue( Hibernate.isInitialized( optionalAssociation ) ); + assertNotNull( optionalAssociation ); + assertEquals( 2, optionalAssociation.size() ); + } ) + ); + } + + @Test + public void testDetachedProxyReference(VertxTestContext context) { + final Book goodOmens = new Book( "Good Omens: The Nice and Accurate Prophecies of Agnes Nutter, Witch" ); + + test( context, getSessionFactory() + .withSession( s -> s.persist( goodOmens ).thenCompose( v -> s.flush() ) ) + .thenCompose( v -> getSessionFactory().withSession( s -> { + Book reference = s.getReference( goodOmens ); + assertFalse( Hibernate.isInitialized( reference ) ); + return completedFuture( reference ); + } ) ) + .thenCompose( reference -> getSessionFactory().withSession( s -> { + Book book = s.getReference( Book.class, reference.getId() ); + assertFalse( Hibernate.isInitialized( book ) ); + assertFalse( Hibernate.isInitialized( reference ) ); + return s.persist( new Author( "Neil Gaiman", book ) ) + .thenCompose( v -> s.flush() ) + .thenApply( v -> reference ); + } ) ) + .thenCompose( reference -> getSessionFactory().withSession( s -> { + Book book = s.getReference( reference ); + assertFalse( Hibernate.isInitialized( book ) ); + assertFalse( Hibernate.isInitialized( reference ) ); + return s.persist( new Author( "Terry Pratchett", book ) ) + .thenCompose( v -> s.flush() ) + .thenApply( v -> reference ); + } ) ) + .thenCompose( reference -> getSessionFactory().withSession( s -> { + Book book = s.getReference( reference ); + assertFalse( Hibernate.isInitialized( book ) ); + assertFalse( Hibernate.isInitialized( reference ) ); + return Stage.fetch( book ).thenCompose( v -> Stage.fetch( book.getAuthors() ) ); + } ) ) + .thenAccept( optionalAssociation -> { + assertTrue( Hibernate.isInitialized( optionalAssociation ) ); + assertNotNull( optionalAssociation ); + assertEquals( 2, optionalAssociation.size() ); + } ) + ); + } + + @Test + public void testRemoveDetachedProxy(VertxTestContext context) { + final Book goodOmens = new Book( "Good Omens: The Nice and Accurate Prophecies of Agnes Nutter, Witch" ); + test( context, getSessionFactory() + .withSession( s -> s.persist( goodOmens ).thenCompose( v -> s.flush() ) ) + .thenCompose( v -> getSessionFactory().withSession( sess -> { + Book reference = sess.getReference( goodOmens ); + assertFalse( Hibernate.isInitialized( reference ) ); + return completedFuture( reference ); + } ) ) + .thenCompose( reference -> getSessionFactory().withSession( s -> s + .remove( s.getReference( reference ) ).thenCompose( v -> s.flush() ) ) + ) + .thenCompose( reference -> getSessionFactory().withSession( s -> s + .find( Book.class, goodOmens.getId() ) + .thenAccept( Assertions::assertNull ) + ) ) + ); + } + + @Test + public void testRemoveWithTransaction(VertxTestContext context) { + final Book goodOmens = new Book( "Good Omens: The Nice and Accurate Prophecies of Agnes Nutter, Witch" ); + test( context, getMutinySessionFactory() + .withTransaction( s -> s.persist( goodOmens ) ) + .call( () -> getMutinySessionFactory() + .withSession( s -> s.find( Book.class, goodOmens.getId() ) ) + .invoke( book -> assertEquals( goodOmens, book ) ) ) + .call( () -> getMutinySessionFactory().withTransaction( s -> s + .remove( s.getReference( Book.class, goodOmens.getId() ) ) ) ) + .call( () -> getMutinySessionFactory().withSession( s -> s + .find( Book.class, goodOmens.getId() ) ) ) + .invoke( Assertions::assertNull ) + ); + } + + @Test + public void testLockDetachedProxy(VertxTestContext context) { + final Book goodOmens = new Book( "Good Omens: The Nice and Accurate Prophecies of Agnes Nutter, Witch" ); + test( context, getSessionFactory().withSession( s -> s + .persist( goodOmens ).thenCompose( v -> s.flush() ) + ) + .thenCompose( v -> getSessionFactory().withSession( s -> { + Book reference = s.getReference( goodOmens ); + assertFalse( Hibernate.isInitialized( reference ) ); + return completedFuture( reference ); + } ) ) + .thenCompose( reference -> getSessionFactory().withSession( s -> s + .lock( s.getReference( reference ), LockMode.PESSIMISTIC_FORCE_INCREMENT ) + .thenCompose( v -> s.flush() ) ) + ) + .thenCompose( v -> getSessionFactory().withSession( s -> s + .find( Book.class, goodOmens.getId() ) ) + .thenAccept( book -> { + assertNotNull( book ); + assertEquals( 2, book.getVersion() ); + } ) + ) + ); + } + + @Test + public void testRefreshDetachedProxy(VertxTestContext context) { + final Book goodOmens = new Book( "Good Omens: The Nice and Accurate Prophecies of Agnes Nutter, Witch" ); + test( context, getSessionFactory().withSession( s -> s + .persist( goodOmens ).thenCompose( v -> s.flush() ) + ) + .thenCompose( v -> getSessionFactory().withSession( s -> { + Book reference = s.getReference( goodOmens ); + assertFalse( Hibernate.isInitialized( reference ) ); + return completedFuture( reference ); + } ) ) + .thenCompose( reference -> getSessionFactory().withSession( s -> s + .refresh( s.getReference( reference ) ) + .thenAccept( v -> assertTrue( Hibernate.isInitialized( s.getReference( reference ) ) ) ) + .thenCompose( v -> s.flush() ) ) + ) + .thenCompose( v -> getSessionFactory().withSession( s -> s + .find( Book.class, goodOmens.getId() ) + .thenAccept( book -> { + assertNotNull( book ); + assertEquals( 1, book.getVersion() ); + } ) + ) ) + ); + } +}