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 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 g = s.createEntityGraph(Employee.class); g.addAttributeNodes("department"); List 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 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 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 ids = s.createSelectionQuery("select d.id from Department d order by d.id", Long.class).setMaxResults(2).getResultList(); List 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()); } }