Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
target
*.iml
.idea
.vscode

# Eclipse
.settings
Expand Down
Original file line number Diff line number Diff line change
@@ -1,55 +1,68 @@
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;
import org.hibernate.testing.orm.junit.ServiceRegistry;
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.
* <p>
* 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());
});
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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 + "]";
}

}
Original file line number Diff line number Diff line change
@@ -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<ChildEntity> 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<ChildEntity> 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<ChildEntity> getChildren() {
return children;
}

}