diff --git a/.gitignore b/.gitignore index 253b7ce11..02471384b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ target *.iml .idea +.vscode # Eclipse .settings diff --git a/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java b/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java index 46f49f32b..6cf0eedbd 100644 --- a/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java +++ b/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/ORMUnitTestCase.java @@ -1,5 +1,9 @@ package org.hibernate.bugs; +import java.util.List; +import org.hibernate.bugs.entities.ChildEntity; +import org.hibernate.bugs.entities.AnotherEntity; +import org.hibernate.bugs.entities.ParentEntity; import org.hibernate.cfg.AvailableSettings; import org.hibernate.testing.orm.junit.DomainModel; @@ -7,49 +11,58 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -/** - * This template demonstrates how to develop a test case for Hibernate ORM, using its built-in unit test framework. - * Although ORMStandaloneTestCase is perfectly acceptable as a reproducer, usage of this class is much preferred. - * Since we nearly always include a regression test with bug fixes, providing your reproducer using this method - * simplifies the process. - *

- * What's even better? Fork hibernate-orm itself, add your test case directly to a module's unit tests, then - * submit it as a PR! - */ -@DomainModel( - annotatedClasses = { - // Add your entities here. - // Foo.class, - // Bar.class - }, - // If you use *.hbm.xml mappings, instead of annotations, add the mappings here. - xmlMappings = { - // "org/hibernate/test/Foo.hbm.xml", - // "org/hibernate/test/Bar.hbm.xml" - } -) -@ServiceRegistry( - // Add in any settings that are specific to your test. See resources/hibernate.properties for the defaults. - settings = { - // For your own convenience to see generated queries: - @Setting(name = AvailableSettings.SHOW_SQL, value = "true"), - @Setting(name = AvailableSettings.FORMAT_SQL, value = "true"), - // @Setting( name = AvailableSettings.GENERATE_STATISTICS, value = "true" ), - - // Add your own settings that are a part of your quarkus configuration: - // @Setting( name = AvailableSettings.SOME_CONFIGURATION_PROPERTY, value = "SOME_VALUE" ), - } -) +@DomainModel(annotatedClasses = { + AnotherEntity.class, + ParentEntity.class, + ChildEntity.class, +}) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.SHOW_SQL, value = "true"), + @Setting(name = AvailableSettings.FORMAT_SQL, value = "true"), +}) @SessionFactory class ORMUnitTestCase { - // Add your tests, using standard JUnit 5. @Test void hhh123Test(SessionFactoryScope scope) throws Exception { - scope.inTransaction( session -> { - // Do stuff... - } ); + // 1st tx : parent initialization with 2 children "a1" and "a2". + var parentId = scope.fromTransaction(session -> { + var parent = new ParentEntity(); + parent.replaceChildren(List.of(new ChildEntity("a1"), new ChildEntity("a2"))); + session.persist(parent); + return parent.getId(); + }); + + scope.inTransaction(session -> { + for (var i = 0; i < 3; i++) { + // In the real-life scenario, we persist a new AnotherEntity before selecting a + // bunch of them, but it turns out + // it's not actually needed to reproduce. + + // execute a HQL query that may trigger a "flush" but is not directly related to + // "ChildEntity". + session.createSelectionQuery("select p.id from AnotherEntity p", String.class).getResultList(); + + // attempt to remove current children and replace them with new ones. + var parent = session.find(ParentEntity.class, parentId); + parent.replaceChildren(List.of(new ChildEntity("b" + i), new ChildEntity("c" + i))); + } + }); + + scope.inTransaction(session -> { + var parent = session.createSelectionQuery("select p from ParentEntity p", ParentEntity.class) + .getSingleResult(); + var allChildren = session.createSelectionQuery("select c from ChildEntity c", ChildEntity.class) + .getResultList(); + + // actual is 6 if annotated with @JoinColumn(updatable=false), 2 otherwise + Assertions.assertEquals(2, parent.getChildren().size()); + + // actual is always 6. Only "a1" and "a2" have actually been deleted. + Assertions.assertEquals(2, allChildren.size()); + }); } } diff --git a/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/AnotherEntity.java b/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/AnotherEntity.java new file mode 100644 index 000000000..fc3334556 --- /dev/null +++ b/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/AnotherEntity.java @@ -0,0 +1,13 @@ +package org.hibernate.bugs.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +@Entity +public class AnotherEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; +} \ No newline at end of file diff --git a/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/ChildEntity.java b/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/ChildEntity.java new file mode 100644 index 000000000..e5ada4564 --- /dev/null +++ b/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/ChildEntity.java @@ -0,0 +1,61 @@ +package org.hibernate.bugs.entities; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +@Entity +public class ChildEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, updatable = false) + private ParentEntity parent; + + private String name; + + public ChildEntity() { + } + + public ChildEntity(String name) { + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public void setParent(ParentEntity parent) { + this.parent = parent; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (!(o instanceof ChildEntity)) + return false; + return id != null && id.equals(((ChildEntity) o).getId()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public String toString() { + return "ChildEntity [id=" + id + ", name=" + name + "]"; + } + +} \ No newline at end of file diff --git a/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/ParentEntity.java b/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/ParentEntity.java new file mode 100644 index 000000000..04ce7ad96 --- /dev/null +++ b/orm/hibernate-orm-7/src/test/java/org/hibernate/bugs/entities/ParentEntity.java @@ -0,0 +1,52 @@ +package org.hibernate.bugs.entities; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +@Entity +public class ParentEntity { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL) + private List children = new ArrayList<>(); + + public void addChild(ChildEntity child) { + child.setParent(this); + children.add(child); + } + + public void removeChild(ChildEntity child) { + child.setParent(null); + children.remove(child); + } + + public void replaceChildren(List nextChildren) { + var prev = new ArrayList<>(children); + prev.forEach(this::removeChild); + nextChildren.forEach(this::addChild); + } + + @Override + public String toString() { + return "ParentEntity [id=" + id + ", children=" + children + "]"; + } + + public String getId() { + return id; + } + + public List getChildren() { + return children; + } + +} \ No newline at end of file