Files

263 lines
11 KiB
Java

package lab.hibernate;
import static org.junit.jupiter.api.Assertions.*;
import jakarta.persistence.*;
import java.util.List;
import lab.hibernate.model.*;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.graph.GraphSemantic;
import org.junit.jupiter.api.Test;
class C_AssociationsTest extends Lab {
private static final Class<?>[] HR = {Department.class, Employee.class, Project.class, EagerEmployee.class};
/** 3 departments with 3 employees each. */
private void seed() {
tx(s -> {
for (int d = 1; d <= 3; d++) {
Department dep = new Department("D" + d);
s.persist(dep);
for (int e = 1; e <= 3; e++) {
s.persist(new Employee("E" + d + e, 100 * e, dep));
s.persist(new EagerEmployee("EE" + d + e, dep));
}
}
});
}
@Test
void q17_defaultFetchTypes() throws Exception {
q(17, "default-fetch-types");
p("default of fetch() on each JPA annotation, read from the API jar:");
for (Class<?> a : List.of(Basic.class, ManyToOne.class, OneToOne.class, OneToMany.class, ManyToMany.class, ElementCollection.class)) {
p(" @%-17s -> %s", a.getSimpleName(), a.getMethod("fetch").getDefaultValue());
}
assertEquals(FetchType.EAGER, ManyToOne.class.getMethod("fetch").getDefaultValue());
assertEquals(FetchType.LAZY, OneToMany.class.getMethod("fetch").getDefaultValue());
}
@Test
void q18_forgettingTheOwningSide() {
q(18, "owning-side");
boot(HR);
tx(s -> {
Department d = new Department("Ops");
Employee e = new Employee("Ivy", 1, null);
d.employees.add(e); // only the inverse side is set
s.persist(d);
s.persist(e);
});
try (Session s = sf.openSession()) {
Object fk = s.createNativeQuery("select department_id from employee", Object.class).getSingleResult();
p("employee.department_id after d.employees.add(e) only: %s", fk);
assertNull(fk);
}
tx(s -> {
Department d = s.createSelectionQuery("from Department", Department.class).getSingleResult();
Employee e = s.createSelectionQuery("from Employee", Employee.class).getSingleResult();
e.department = d; // set the owning side (mappedBy points here)
d.employees.add(e);
});
try (Session s = sf.openSession()) {
Object fk = s.createNativeQuery("select department_id from employee", Object.class).getSingleResult();
p("after setting employee.department too : %s", fk);
assertNotNull(fk);
}
}
@Test
void q19_nPlusOne() {
q(19, "n-plus-one");
boot(HR);
seed();
int m = SqlLog.mark();
tx(s -> {
for (Department d : s.createSelectionQuery("from Department", Department.class).getResultList()) {
d.getEmployees().size();
}
});
p("naive loop over 3 departments: %d SQL statements (1 + 3)", SqlLog.since(m).size());
sql(m);
int m2 = SqlLog.mark();
tx(s -> {
for (Department d : s.createSelectionQuery("select distinct d from Department d join fetch d.employees", Department.class).getResultList()) {
d.getEmployees().size();
}
});
p("with join fetch: %d SQL statement", SqlLog.since(m2).size());
sql(m2);
assertEquals(1, SqlLog.since(m2).size());
}
@Test
void q20_joinFetchVersusEntityGraph() {
q(20, "join-fetch-vs-entity-graph");
boot(HR);
seed();
int m = SqlLog.mark();
tx(s -> {
List<Employee> list = s.createSelectionQuery("select e from Employee e join fetch e.department", Employee.class).getResultList();
p("join fetch : department initialized = %s", Hibernate.isInitialized(list.get(0).getDepartment()));
});
sql(m);
int m2 = SqlLog.mark();
tx(s -> {
EntityGraph<Employee> g = s.createEntityGraph(Employee.class);
g.addAttributeNodes("department");
List<Employee> list = s.createSelectionQuery("from Employee", Employee.class).setEntityGraph(g, GraphSemantic.FETCH).getResultList();
p("entity graph : department initialized = %s", Hibernate.isInitialized(list.get(0).getDepartment()));
});
sql(m2);
}
@Test
void q21_batchFetching() {
q(21, "batch-fetch-size");
boot(new String[] {"hibernate.default_batch_fetch_size", "2"}, HR);
seed();
int m = SqlLog.mark();
tx(s -> {
for (Department d : s.createSelectionQuery("from Department", Department.class).getResultList()) {
d.getEmployees().size();
}
});
p("default_batch_fetch_size=2, 3 departments: %d SQL statements (1 + ceil(3/2))", SqlLog.since(m).size());
sql(m);
assertEquals(3, SqlLog.since(m).size());
}
@Test
void q22_lazyInitializationException() {
q(22, "lazy-initialization-exception");
boot(HR);
seed();
Department d = txr(s -> s.createSelectionQuery("from Department order by id", Department.class).getResultList().get(0));
try {
d.getEmployees().size();
} catch (RuntimeException ex) {
p("read a lazy collection after the session closed -> %s", first(ex));
}
int m = SqlLog.mark();
int n = txr(s -> {
Department fresh = s.createSelectionQuery("select d from Department d join fetch d.employees where d.id = :id", Department.class).setParameter("id", d.getId()).getSingleResult();
return fresh.getEmployees().size();
});
p("cure 1: fetch it inside the query -> %d employees, %d SQL statement", n, SqlLog.since(m).size());
int m2 = SqlLog.mark();
List<String> names = txr(s -> s.createSelectionQuery("select e.name from Employee e where e.department.id = :id order by e.name", String.class).setParameter("id", d.getId()).getResultList());
p("cure 2: ask for exactly the columns you need -> %s, %d SQL statement", names, SqlLog.since(m2).size());
}
@Test
void q23_multipleBagFetchException() {
q(23, "multiple-bag-fetch");
boot(Post.class, Comment.class);
tx(s -> {
Post post = new Post("p");
post.getTags().add("java");
post.getTags().add("jpa");
post.getComments().add(new Comment("c1", post));
post.getComments().add(new Comment("c2", post));
s.persist(post);
});
try {
tx(s -> s.createSelectionQuery("select p from Post p join fetch p.comments join fetch p.tags", Post.class).getResultList());
} catch (RuntimeException ex) {
p("two List collections in one join fetch -> %s", first(ex));
}
int m = SqlLog.mark();
tx(s -> {
Post post = s.createSelectionQuery("select p from Post p join fetch p.comments", Post.class).getSingleResult();
s.createSelectionQuery("select p from Post p join fetch p.tags where p in :p", Post.class).setParameter("p", post).getSingleResult();
p("fix: one collection per query -> comments=%d tags=%d, %d SQL statements", post.getComments().size(), post.getTags().size(), SqlLog.since(m).size());
});
}
@Test
void q24_paginationWithCollectionFetch() {
q(24, "pagination-join-fetch");
boot(HR);
seed();
int m = SqlLog.mark();
tx(s -> {
List<Department> page = s.createSelectionQuery("select d from Department d join fetch d.employees order by d.id", Department.class).setMaxResults(2).getResultList();
p("join fetch of a collection + setMaxResults(2) -> %d departments", page.size());
});
p("SQL contains a row limit: %s", SqlLog.since(m).stream().anyMatch(x -> x.toLowerCase().contains("fetch first") || x.toLowerCase().contains(" limit ")));
sql(m);
int m2 = SqlLog.mark();
tx(s -> {
List<Long> ids = s.createSelectionQuery("select d.id from Department d order by d.id", Long.class).setMaxResults(2).getResultList();
List<Department> page = s.createSelectionQuery("select distinct d from Department d join fetch d.employees where d.id in :ids order by d.id", Department.class).setParameter("ids", ids).getResultList();
p("two-step fix -> %d departments", page.size());
});
sql(m2);
}
@Test
void q25_cascadeAndTransientInstances() {
q(25, "cascade-transient");
boot(HR);
try {
tx(s -> s.persist(new Employee("Jo", 1, new Department("Unsaved"))));
} catch (RuntimeException ex) {
p("persist Employee that points at an unsaved Department -> %s", first(ex));
}
boot2();
tx(s -> {
Post post = new Post("with cascade");
post.getComments().add(new Comment("c", post));
s.persist(post); // CascadeType.ALL on Post.comments saves the comment too
});
long stored = txr(s -> s.createSelectionQuery("select count(c) from Comment c", Long.class).getSingleResult());
p("Post -> comments has CascadeType.ALL: comments stored = %d", stored);
}
private void boot2() {
sf.close();
boot(Post.class, Comment.class);
}
@Test
void q26_orphanRemoval() {
q(26, "orphan-removal");
boot(PurchaseOrder.class, OrderLine.class);
Long id = txr(s -> {
PurchaseOrder o = new PurchaseOrder("acme");
o.addLine(new OrderLine("A"));
o.addLine(new OrderLine("B"));
s.persist(o);
return o.getId();
});
int m = SqlLog.mark();
tx(s -> {
PurchaseOrder o = s.find(PurchaseOrder.class, id);
o.getLines().remove(0); // no remove() call on the OrderLine
});
p("removed a line from the collection only:");
sql(m);
int m2 = SqlLog.mark();
tx(s -> s.remove(s.find(PurchaseOrder.class, id)));
p("removed the order (cascade ALL):");
sql(m2);
}
@Test
void q27_eagerManyToOneDefault() {
q(27, "eager-many-to-one");
boot(HR);
seed();
int m = SqlLog.mark();
tx(s -> s.createSelectionQuery("from EagerEmployee", EagerEmployee.class).getResultList());
p("'from EagerEmployee' with the default @ManyToOne: %d SQL statements for 9 employees in 3 departments", SqlLog.since(m).size());
sql(m);
int m2 = SqlLog.mark();
tx(s -> s.createSelectionQuery("from Employee", Employee.class).getResultList());
p("'from Employee' with @ManyToOne(fetch = LAZY): %d SQL statement", SqlLog.since(m2).size());
assertEquals(1, SqlLog.since(m2).size());
}
}