Add hibernate-jpa module: 40 runnable Hibernate 7 / JPA interview answers on PostgreSQL 16
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cfo2K4u4rF18QsPXPFnytf
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user