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:
2026-10-24 17:33:29 +00:00
co-authored by Claude Sonnet 5.5
parent a5fbf45971
commit d03962ea9c
87 changed files with 2387 additions and 1 deletions
+2 -1
View File
@@ -9,7 +9,8 @@ One module per article. This repository currently holds:
| Module | Article | Status |
|---|---|---|
| [`collections`](collections/) | Top 40 Java Collections Interview Questions (HashMap Internals to Fail-Fast Iterators) | published |
| [`lld-parking-lot`](lld-parking-lot/) | Low-Level Design Interview in Java: Parking Lot System, Step by Step | this commit |
| [`lld-parking-lot`](lld-parking-lot/) | Low-Level Design Interview in Java: Parking Lot System, Step by Step | published |
| [`hibernate-jpa`](hibernate-jpa/) | Top 40 Hibernate and JPA Interview Questions (2026) with Runnable Answers | this commit |
## `collections` module
+1
View File
@@ -0,0 +1 @@
target/
+19
View File
@@ -0,0 +1,19 @@
# hibernate-jpa
Companion code for **Top 40 Hibernate and JPA Interview Questions (2026) with Runnable Answers** on ankurm.com.
One test per question (`src/test/java/lab/hibernate/A_...F_*`). Each test writes its own transcript to `output/qNN-*.txt` and asserts the same facts, so an answer in the article that stops being true fails the build.
| Component | Version |
|---|---|
| Hibernate ORM | 7.4.13.Final |
| PostgreSQL | 16.15 (server), pgjdbc 42.7.14 |
| JUnit | 6.1.3 |
| JDK | 25.0.5 |
```bash
./scripts/start-postgres.sh # throwaway PostgreSQL on :5433 (or point LAB_DB_URL at your own)
./scripts/run-all.sh # runs 40 tests, rewrites output/
```
The test JVM runs with `-Duser.timezone=UTC` (see `pom.xml`): a PostgreSQL build without a complete tz database rejects some JVM default zones at connect time.
@@ -0,0 +1,4 @@
Session is an EntityManager : true
SessionFactory is an EMF : true
em.unwrap(Session.class) == em : true
Hibernate-only API reachable : s.getStatistics().isStatisticsEnabled() = true
@@ -0,0 +1,5 @@
new Employee() : managed=false id=null
after persist() : managed=true id assigned=true
after detach() : managed=false
after merge() : original managed=false, returned copy managed=true, same object=false
after remove() : managed=false
@@ -0,0 +1,3 @@
persist(detached) -> jakarta.persistence.EntityExistsException: Detached entity passed to persist: lab.hibernate.model.Employee
merge(detached) -> returns a different instance: true
salary changed on the original after merge is stored: false (db has 100)
@@ -0,0 +1,2 @@
methods present on org.hibernate.Session : [find, get, getReference, load, merge, persist, refresh, remove]
methods NOT present : [delete, save, saveOrUpdate, update]
@@ -0,0 +1,7 @@
getReference(): SQL statements so far = 0, initialized = false
ref.name (field) -> null; SQL statements now = 0, initialized = false
ref.getName() -> Chen; SQL statements now = 1, initialized = true
SQL> select e1_0.id,e1_0.city,e1_0.zip,e1_0.department_id,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.id=?
find(missing id) -> null
getReference(missing) -> proxy returned without error: true
first access -> org.hibernate.ObjectNotFoundException: No row with the given identifier exists for entity [lab.hibernate.model.Employee with id '99999']
@@ -0,0 +1,8 @@
change a managed entity, no save call:
SQL> select e1_0.id,e1_0.city,e1_0.zip,e1_0.department_id,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.id=?
SQL> update Employee set city=?,zip=?,department_id=?,level=?,name=?,salary=? where id=?
count visible inside the transaction (AUTO flush before the query): 2
persist then query:
SQL> select nextval('Employee_SEQ')
SQL> insert into Employee (city,zip,department_id,level,name,salary,id) values (?,?,?,?,?,?,?)
SQL> select count(e1_0.id) from Employee e1_0
@@ -0,0 +1,2 @@
refresh(): pending in-memory change thrown away, salary = 100
merge(detached with salary 500) -> database now has 500
@@ -0,0 +1,5 @@
two find() calls, same session: SQL selects = 1, same instance = true
after clear(): SQL selects = 2, same instance as before = false
SQL> select e1_0.id,e1_0.city,e1_0.zip,e1_0.department_id,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.id=?
SQL> select e1_0.id,e1_0.city,e1_0.zip,e1_0.department_id,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.id=?
a different session: SQL selects = 1 (the first-level cache is per session)
@@ -0,0 +1,4 @@
SessionFactory started with an entity that has no no-arg constructor: true
persist(): ok
find() -> org.hibernate.InstantiationException: No default constructor for entity 'lab.hibernate.model.BadEntity'
root cause -> org.hibernate.InstantiationException: No default constructor for entity 'lab.hibernate.model.BadEntity'
@@ -0,0 +1,2 @@
SEQUENCE: 25 entities -> 1 prepared insert statement(s) with jdbc.batch_size=10, 2 sequence selects
IDENTITY: 25 entities -> 25 prepared insert statements (one per row)
@@ -0,0 +1 @@
columns of table employee: [city, department_id, id, level, name, salary, zip]
@@ -0,0 +1,2 @@
@Enumerated(STRING) stores LEAD as: LEAD
@Enumerated (default) stores LEAD as: 2 (its position in the enum: [JUNIOR, SENIOR, LEAD])
@@ -0,0 +1 @@
tables: [comment, post, post_tags]
@@ -0,0 +1,5 @@
tables: [bankpayment, cardpayment, payment, shape]
JOINED 'from Payment' returned 2 rows
SINGLE_TABLE 'from Shape' returned 2 rows
SQL> select p1_0.id,case when p1_2.id is not null then 1 when p1_1.id is not null then 2 end,p1_0.amount,p1_1.iban,p1_2.last4 from Payment p1_0 left join BankPayment p1_1 on p1_0.id=p1_1.id left join CardPayment p1_2 on p1_0.id=p1_2.id
SQL> select s1_0.id,s1_0.kind,s1_0.label,s1_0.radius,s1_0.side from Shape s1_0
@@ -0,0 +1,3 @@
tables: [note]
select from the entity Note: 1 row(s), created_by column inherited = system
select from the @MappedSuperclass -> org.hibernate.query.sqm.UnknownEntityException: Could not resolve root entity 'Auditable'
@@ -0,0 +1,2 @@
before persist: id=null, set.contains(item)=true
after persist : id assigned=true, hashCode changed, set.contains(item)=false
@@ -0,0 +1,7 @@
default of fetch() on each JPA annotation, read from the API jar:
@Basic -> EAGER
@ManyToOne -> EAGER
@OneToOne -> EAGER
@OneToMany -> LAZY
@ManyToMany -> LAZY
@ElementCollection -> LAZY
+2
View File
@@ -0,0 +1,2 @@
employee.department_id after d.employees.add(e) only: null
after setting employee.department too : 1
+7
View File
@@ -0,0 +1,7 @@
naive loop over 3 departments: 4 SQL statements (1 + 3)
SQL> select d1_0.id,d1_0.name from Department d1_0
SQL> select e1_0.department_id,e1_0.id,e1_0.city,e1_0.zip,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.department_id=?
SQL> select e1_0.department_id,e1_0.id,e1_0.city,e1_0.zip,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.department_id=?
SQL> select e1_0.department_id,e1_0.id,e1_0.city,e1_0.zip,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.department_id=?
with join fetch: 1 SQL statement
SQL> select distinct d1_0.id,e1_0.department_id,e1_0.id,e1_0.city,e1_0.zip,e1_0.level,e1_0.name,e1_0.salary,d1_0.name from Department d1_0 join Employee e1_0 on d1_0.id=e1_0.department_id
@@ -0,0 +1,4 @@
join fetch : department initialized = true
SQL> select e1_0.id,e1_0.city,e1_0.zip,d1_0.id,d1_0.name,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 join Department d1_0 on d1_0.id=e1_0.department_id
entity graph : department initialized = true
SQL> select e1_0.id,e1_0.city,e1_0.zip,d1_0.id,d1_0.name,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 left join Department d1_0 on d1_0.id=e1_0.department_id
@@ -0,0 +1,4 @@
default_batch_fetch_size=2, 3 departments: 3 SQL statements (1 + ceil(3/2))
SQL> select d1_0.id,d1_0.name from Department d1_0
SQL> select e1_0.department_id,e1_0.id,e1_0.city,e1_0.zip,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.department_id = any (?)
SQL> select e1_0.department_id,e1_0.id,e1_0.city,e1_0.zip,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.department_id=?
@@ -0,0 +1,3 @@
read a lazy collection after the session closed -> org.hibernate.LazyInitializationException: Cannot lazily initialize collection of role 'lab.hibernate.model.Department.employees' with key '1' (no session)
cure 1: fetch it inside the query -> 3 employees, 1 SQL statement
cure 2: ask for exactly the columns you need -> [E11, E12, E13], 1 SQL statement
@@ -0,0 +1,2 @@
two List collections in one join fetch -> java.lang.IllegalArgumentException: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [lab.hibernate.model.Post.comments, lab.hibernate.model.Post.tags]
fix: one collection per query -> comments=2 tags=2, 2 SQL statements
@@ -0,0 +1,6 @@
join fetch of a collection + setMaxResults(2) -> 2 departments
SQL contains a row limit: true
SQL> select d1_0.id,e1_0.department_id,e1_0.id,e1_0.city,e1_0.zip,e1_0.level,e1_0.name,e1_0.salary,d1_0.name from (select d1_0.id,d1_0.name from Department d1_0 where exists(select 1 from Employee e1_0 where d1_0.id=e1_0.department_id) order by d1_0.id offset ? rows fetch first ? rows only) d1_0(id,name) join Employee e1_0 on d1_0.id=e1_0.department_id order by d1_0.id
two-step fix -> 2 departments
SQL> select d1_0.id from Department d1_0 order by d1_0.id fetch first ? rows only
SQL> select distinct d1_0.id,e1_0.department_id,e1_0.id,e1_0.city,e1_0.zip,e1_0.level,e1_0.name,e1_0.salary,d1_0.name from Department d1_0 join Employee e1_0 on d1_0.id=e1_0.department_id where d1_0.id in (?,?) order by d1_0.id
@@ -0,0 +1,2 @@
persist Employee that points at an unsaved Department -> java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: Persistent instance of 'lab.hibernate.model.Employee' references an unsaved transient instance of 'lab.hibernate.model.Department' (persist the transient instance before flushing) [lab.hibernate.model.Employee.department -> lab.hibernate.model.Department]
Post -> comments has CascadeType.ALL: comments stored = 1
@@ -0,0 +1,9 @@
removed a line from the collection only:
SQL> select po1_0.id,po1_0.customer from PurchaseOrder po1_0 where po1_0.id=?
SQL> select l1_0.order_id,l1_0.id,l1_0.sku from OrderLine l1_0 where l1_0.order_id=?
SQL> delete from OrderLine where id=?
removed the order (cascade ALL):
SQL> select po1_0.id,po1_0.customer from PurchaseOrder po1_0 where po1_0.id=?
SQL> select l1_0.order_id,l1_0.id,l1_0.sku from OrderLine l1_0 where l1_0.order_id=?
SQL> delete from OrderLine where id=?
SQL> delete from PurchaseOrder where id=?
@@ -0,0 +1,6 @@
'from EagerEmployee' with the default @ManyToOne: 4 SQL statements for 9 employees in 3 departments
SQL> select ee1_0.id,ee1_0.department_id,ee1_0.name from EagerEmployee ee1_0
SQL> select d1_0.id,d1_0.name from Department d1_0 where d1_0.id=?
SQL> select d1_0.id,d1_0.name from Department d1_0 where d1_0.id=?
SQL> select d1_0.id,d1_0.name from Department d1_0 where d1_0.id=?
'from Employee' with @ManyToOne(fetch = LAZY): 1 SQL statement
@@ -0,0 +1,6 @@
right after persist() the row is already in L2 (READ_WRITE caches on insert): SQL selects = 0, hits = 1
after evictAllRegions(), three find() calls, three different sessions:
SQL selects : 1
L2 cache hits : 2
L2 cache misses : 1
L2 cache puts : 1
+5
View File
@@ -0,0 +1,5 @@
run 1: query cache hits=0 misses=1 puts=1
run 2: query cache hits=1 misses=1 puts=1
SQL selects for Product after 2 runs: 1
run 3 after inserting a Product: query cache hits=1 misses=2
SQL selects for Product after 3 runs: 3
@@ -0,0 +1,3 @@
after an HQL bulk update to 11 : find() sees 11 (L2 hits=0 misses=1)
after a native bulk update to 12 : find() sees 12 (L2 hits=0 misses=1)
after raw JDBC update to 13 : find() sees 12 (L2 hits=1 misses=0) <- stale: Hibernate was not told
@@ -0,0 +1,6 @@
both sessions read balance=100 version=0
session A commits +50 -> version is now 1
session B commits +25 -> jakarta.persistence.OptimisticLockException: Unexpected row count (expected row count 1 but was 0) [update Account set balance=?,owner=?,version=? where id=? and version=?] for entity [lab.hibernate.model.Account with id '1']
cause: org.hibernate.StaleObjectStateException: Unexpected row count (expected row count 1 but was 0) [update Account set balance=?,owner=?,version=? where id=? and version=?] for entity [lab.hibernate.model.Account with id '1']
SQL> update Account set balance=?,owner=?,version=? where id=? and version=?
final balance: 150 (B's stale write was rejected, A's +50 kept)
@@ -0,0 +1,3 @@
session 1: find(..., PESSIMISTIC_WRITE) ran: select a1_0.id,a1_0.balance,a1_0.owner,a1_0.version from Account a1_0 where a1_0.id=? for no key update of a1_0
session 2 with lock.timeout=0 -> jakarta.persistence.PessimisticLockException: JDBC exception executing SQL [ERROR: could not obtain lock on row in relation "account"] [select a1_0.id,a1_0.balance,a1_0.owner,a1_0.version from Account a1_0 where a1_0.id=? for no key update of a1_0 nowait]
cause: org.hibernate.exception.LockTimeoutException: JDBC exception executing SQL [ERROR: could not obtain lock on row in relation "account"] [select a1_0.id,a1_0.balance,a1_0.owner,a1_0.version from Account a1_0 where a1_0.id=? for no key update of a1_0 nowait]
+2
View File
@@ -0,0 +1,2 @@
SQL> update PlainAccount set balance=?,owner=? where id=?
two deposits (+50, +25) on 100 -> final balance 125, expected 175: A's deposit silently lost
@@ -0,0 +1,3 @@
PostgreSQL default isolation level: read committed
read committed : first read 100, another transaction commits 150, second read 150 -> non-repeatable read
repeatable read : first read 100, another transaction commits 150, second read 100 -> repeatable
@@ -0,0 +1,2 @@
Wallet (owns a unidirectional @OneToMany): add a Coin -> version 0 -> 1
Folder (inverse, mappedBy @OneToMany) : add a Doc -> version 0 -> 0
@@ -0,0 +1,6 @@
default entity, change one column:
SQL> select p1_0.id,p1_0.a,p1_0.b,p1_0.c from Plain p1_0 where p1_0.id=?
SQL> update Plain set a=?,b=?,c=? where id=?
@DynamicUpdate entity, change one column:
SQL> select w1_0.id,w1_0.a,w1_0.b,w1_0.c from Wide w1_0 where w1_0.id=?
SQL> update Wide set a=? where id=?
@@ -0,0 +1,6 @@
HQL -> [Cy, Ana]
Criteria -> [Cy, Ana]
Native -> [Cy, Ana]
SQL> select e1_0.id,e1_0.city,e1_0.zip,e1_0.department_id,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.salary>? order by e1_0.salary
SQL> select e1_0.id,e1_0.city,e1_0.zip,e1_0.department_id,e1_0.level,e1_0.name,e1_0.salary from Employee e1_0 where e1_0.salary>? order by e1_0.salary
SQL> select * from employee where salary > 150 order by salary
@@ -0,0 +1,4 @@
record projection -> [EmployeeRow[name=Bo, salary=100], EmployeeRow[name=Cy, salary=200], EmployeeRow[name=Ana, salary=300]]
entities loaded: 0
SQL> select e1_0.name,e1_0.salary from Employee e1_0 order by e1_0.salary
full entity query loads 3 entities
@@ -0,0 +1,3 @@
HQL bulk update touched 3 rows
the entity already loaded still says salary = 100
after refresh() it says salary = 999
@@ -0,0 +1,3 @@
buildSessionFactory() failed with org.hibernate.query.NamedQueryValidationException:
Errors in named queries:
[1] Error in query named 'BrokenQuery.byTitle': Could not resolve attribute 'titel' of 'lab.hibernate.model.BrokenQuery' [select b from BrokenQuery b where b.titel = :t]
+85
View File
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ankurm.interview</groupId>
<artifactId>hibernate-jpa</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hibernate.version>7.4.13.Final</hibernate.version>
<junit.version>6.1.3</junit.version>
<postgresql.version>42.7.14</postgresql.version>
<slf4j.version>2.0.20</slf4j.version>
<lab.db.url>jdbc:postgresql://localhost:5433/lab</lab.db.url>
</properties>
<dependencies>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jcache</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>3.11.1</version>
<classifier>jakarta</classifier>
<exclusions>
<exclusion><groupId>*</groupId><artifactId>*</artifactId></exclusion>
</exclusions>
</dependency>
<!-- ehcache's own pom still lists javax JAXB artifacts from blocked repositories; the jakarta ones below replace them -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>4.0.2</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.5</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>${postgresql.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.6</version>
<configuration>
<argLine>-Duser.timezone=UTC</argLine>
<systemPropertyVariables>
<lab.db.url>${lab.db.url}</lab.db.url>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Regenerates every file in hibernate-jpa/output/ (one per interview question).
# Requires PostgreSQL on localhost:5433 with a database "lab" (see start-postgres.sh), JDK 25 and Maven.
set -euo pipefail
cd "$(dirname "$0")/.."
unset JAVA_TOOL_OPTIONS
rm -f output/q*.txt
mvn -B -q test -Dlab.db.url="${LAB_DB_URL:-jdbc:postgresql://localhost:5433/lab}"
echo "done: $(ls output | wc -l) files in output/"
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Starts a throwaway PostgreSQL on port 5433 with a database called "lab" (needs PostgreSQL binaries installed).
# Run as a non-root user. Adjust PGBIN for your platform.
set -euo pipefail
PGBIN="${PGBIN:-/usr/lib/postgresql/16/bin}"
DATA="${PGDATA_DIR:-/tmp/pg-lab/data}"
mkdir -p "$(dirname "$DATA")"
[ -d "$DATA" ] || "$PGBIN/initdb" -D "$DATA" -A trust -U postgres >/dev/null
"$PGBIN/pg_ctl" -D "$DATA" -o "-p 5433 -k /tmp" -l "$(dirname "$DATA")/pg.log" start
sleep 2
psql -h localhost -p 5433 -U postgres -tc "select 1 from pg_database where datname='lab'" | grep -q 1 \
|| psql -h localhost -p 5433 -U postgres -c "create database lab"
@@ -0,0 +1,28 @@
package lab.hibernate;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.hibernate.resource.jdbc.spi.StatementInspector;
/** Records every SQL string Hibernate hands to JDBC, so tests can print and count them. */
public class SqlLog implements StatementInspector {
public static final List<String> LINES = new CopyOnWriteArrayList<>();
@Override
public String inspect(String sql) {
LINES.add(sql);
return sql;
}
public static int mark() {
return LINES.size();
}
public static List<String> since(int mark) {
return List.copyOf(LINES.subList(mark, LINES.size()));
}
public static long countSince(int mark, String prefix) {
return since(mark).stream().filter(s -> s.toLowerCase().startsWith(prefix)).count();
}
}
@@ -0,0 +1,38 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
public class Account {
@Id
@GeneratedValue
public Long id;
public String owner;
public long balance;
@Version
public int version;
protected Account() {}
public Account(String owner, long balance) {
this.owner = owner;
this.balance = balance;
}
public Long getId() {
return id;
}
public String getOwner() {
return owner;
}
public long getBalance() {
return balance;
}
public int getVersion() {
return version;
}
}
@@ -0,0 +1,16 @@
package lab.hibernate.model;
import jakarta.persistence.Embeddable;
@Embeddable
public class Address {
public String city;
public String zip;
protected Address() {}
public Address(String city, String zip) {
this.city = city;
this.zip = zip;
}
}
@@ -0,0 +1,13 @@
package lab.hibernate.model;
import jakarta.persistence.MappedSuperclass;
/** Shared columns, but not an entity: it cannot be queried. */
@MappedSuperclass
public abstract class Auditable {
public String createdBy = "system";
public String getCreatedBy() {
return createdBy;
}
}
@@ -0,0 +1,14 @@
package lab.hibernate.model;
import jakarta.persistence.*;
/** No no-argument constructor at all. */
@Entity
public class BadEntity {
@Id
public Long id;
public BadEntity(Long id) {
this.id = id;
}
}
@@ -0,0 +1,19 @@
package lab.hibernate.model;
import jakarta.persistence.Entity;
@Entity
public class BankPayment extends Payment {
public String iban;
protected BankPayment() {}
public BankPayment(long amount, String iban) {
super(amount);
this.iban = iban;
}
public String getIban() {
return iban;
}
}
@@ -0,0 +1,12 @@
package lab.hibernate.model;
import jakarta.persistence.*;
/** A named query that cannot compile: it should fail when the SessionFactory starts, not when it is first used. */
@Entity
@NamedQuery(name = "BrokenQuery.byTitle", query = "select b from BrokenQuery b where b.titel = :t")
public class BrokenQuery {
@Id
public Long id;
public String title;
}
@@ -0,0 +1,19 @@
package lab.hibernate.model;
import jakarta.persistence.Entity;
@Entity
public class CardPayment extends Payment {
public String last4;
protected CardPayment() {}
public CardPayment(long amount, String last4) {
super(amount);
this.last4 = last4;
}
public String getLast4() {
return last4;
}
}
@@ -0,0 +1,19 @@
package lab.hibernate.model;
import jakarta.persistence.Entity;
@Entity
public class Circle extends Shape {
public Integer radius;
protected Circle() {}
public Circle(String label, int radius) {
super(label);
this.radius = radius;
}
public Integer getRadius() {
return radius;
}
}
@@ -0,0 +1,25 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
public class Coin {
@Id
@GeneratedValue
public Long id;
public int cents;
protected Coin() {}
public Coin(int cents) {
this.cents = cents;
}
public Long getId() {
return id;
}
public int getCents() {
return cents;
}
}
@@ -0,0 +1,33 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
public class Comment {
@Id
@GeneratedValue
public Long id;
public String text;
@ManyToOne(fetch = FetchType.LAZY)
public Post post;
protected Comment() {}
public Comment(String text, Post post) {
this.text = text;
this.post = post;
}
public Long getId() {
return id;
}
public String getText() {
return text;
}
public Post getPost() {
return post;
}
}
@@ -0,0 +1,35 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Department {
@Id
@GeneratedValue
public Long id;
public String name;
/** Inverse side: the foreign key lives in Employee.department_id. Lazy by default. */
@OneToMany(mappedBy = "department")
public List<Employee> employees = new ArrayList<>();
protected Department() {}
public Department(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public List<Employee> getEmployees() {
return employees;
}
}
@@ -0,0 +1,33 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
public class Doc {
@Id
@GeneratedValue
public Long id;
public String title;
@ManyToOne(fetch = FetchType.LAZY)
public Folder folder;
protected Doc() {}
public Doc(String title, Folder folder) {
this.title = title;
this.folder = folder;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public Folder getFolder() {
return folder;
}
}
@@ -0,0 +1,34 @@
package lab.hibernate.model;
import jakarta.persistence.*;
/** Same shape as Employee but with the JPA default for @ManyToOne (EAGER). */
@Entity
public class EagerEmployee {
@Id
@GeneratedValue
public Long id;
public String name;
@ManyToOne
public Department department;
protected EagerEmployee() {}
public EagerEmployee(String name, Department department) {
this.name = name;
this.department = department;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Department getDepartment() {
return department;
}
}
@@ -0,0 +1,63 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Entity
public class Employee {
@Id
@GeneratedValue
public Long id;
public String name;
public int salary;
/** Owning side of the association, lazy on purpose. */
@ManyToOne(fetch = FetchType.LAZY)
public Department department;
@Embedded
public Address address;
@Enumerated(EnumType.STRING)
public Level level = Level.JUNIOR;
@ManyToMany
public Set<Project> projects = new HashSet<>();
protected Employee() {}
public Employee(String name, int salary, Department department) {
this.name = name;
this.salary = salary;
this.department = department;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public int getSalary() {
return salary;
}
public Department getDepartment() {
return department;
}
public Address getAddress() {
return address;
}
public Level getLevel() {
return level;
}
public Set<Project> getProjects() {
return projects;
}
}
@@ -0,0 +1,42 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
/** Versioned parent with an INVERSE (mappedBy) collection: changing the children does not bump the parent. */
@Entity
public class Folder {
@Id
@GeneratedValue
public Long id;
public String name;
@Version
public int version;
@OneToMany(mappedBy = "folder", cascade = CascadeType.ALL)
public List<Doc> docs = new ArrayList<>();
protected Folder() {}
public Folder(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public int getVersion() {
return version;
}
public List<Doc> getDocs() {
return docs;
}
}
@@ -0,0 +1,26 @@
package lab.hibernate.model;
import jakarta.persistence.*;
/** IDENTITY: the database assigns the id, so Hibernate must insert immediately. */
@Entity
public class Invoice {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
public Long id;
public int number;
protected Invoice() {}
public Invoice(int number) {
this.number = number;
}
public Long getId() {
return id;
}
public int getNumber() {
return number;
}
}
@@ -0,0 +1,3 @@
package lab.hibernate.model;
public enum Level { JUNIOR, SENIOR, LEAD }
@@ -0,0 +1,25 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
public class Note extends Auditable {
@Id
@GeneratedValue
public Long id;
public String text;
protected Note() {}
public Note(String text) {
this.text = text;
}
public Long getId() {
return id;
}
public String getText() {
return text;
}
}
@@ -0,0 +1,32 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
public class OrderLine {
@Id
@GeneratedValue
public Long id;
public String sku;
@ManyToOne(fetch = FetchType.LAZY)
public PurchaseOrder order;
protected OrderLine() {}
public OrderLine(String sku) {
this.sku = sku;
}
public Long getId() {
return id;
}
public String getSku() {
return sku;
}
public PurchaseOrder getOrder() {
return order;
}
}
@@ -0,0 +1,26 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Payment {
@Id
@GeneratedValue
public Long id;
public long amount;
protected Payment() {}
protected Payment(long amount) {
this.amount = amount;
}
public Long getId() {
return id;
}
public long getAmount() {
return amount;
}
}
@@ -0,0 +1,35 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
public class Plain {
@Id
@GeneratedValue
public Long id;
public String a = "a";
public String b = "b";
public String c = "c";
protected Plain() {}
public Plain(String a) {
this.a = a;
}
public Long getId() {
return id;
}
public String getA() {
return a;
}
public String getB() {
return b;
}
public String getC() {
return c;
}
}
@@ -0,0 +1,32 @@
package lab.hibernate.model;
import jakarta.persistence.*;
/** No @Version: the lost-update demo. */
@Entity
public class PlainAccount {
@Id
@GeneratedValue
public Long id;
public String owner;
public long balance;
protected PlainAccount() {}
public PlainAccount(String owner, long balance) {
this.owner = owner;
this.balance = balance;
}
public Long getId() {
return id;
}
public String getOwner() {
return owner;
}
public long getBalance() {
return balance;
}
}
@@ -0,0 +1,42 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
/** Two List collections on one entity: the MultipleBagFetchException setup. */
@Entity
public class Post {
@Id
@GeneratedValue
public Long id;
public String title;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL)
public List<Comment> comments = new ArrayList<>();
@ElementCollection
public List<String> tags = new ArrayList<>();
protected Post() {}
public Post(String title) {
this.title = title;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
public List<Comment> getComments() {
return comments;
}
public List<String> getTags() {
return tags;
}
}
@@ -0,0 +1,35 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
@Entity
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {
@Id
@GeneratedValue
public Long id;
public String name;
public int price;
protected Product() {}
public Product(String name, int price) {
this.name = name;
this.price = price;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public int getPrice() {
return price;
}
}
@@ -0,0 +1,25 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
public class Project {
@Id
@GeneratedValue
public Long id;
public String title;
protected Project() {}
public Project(String title) {
this.title = title;
}
public Long getId() {
return id;
}
public String getTitle() {
return title;
}
}
@@ -0,0 +1,39 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class PurchaseOrder {
@Id
@GeneratedValue
public Long id;
public String customer;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
public List<OrderLine> lines = new ArrayList<>();
protected PurchaseOrder() {}
public PurchaseOrder(String customer) {
this.customer = customer;
}
public void addLine(OrderLine line) {
lines.add(line);
line.order = this;
}
public Long getId() {
return id;
}
public String getCustomer() {
return customer;
}
public List<OrderLine> getLines() {
return lines;
}
}
@@ -0,0 +1,33 @@
package lab.hibernate.model;
import jakarta.persistence.*;
/** @Enumerated defaults to ORDINAL: the database stores the constant's position. */
@Entity
public class RankedEmployee {
@Id
@GeneratedValue
public Long id;
public String name;
@Enumerated
public Level level = Level.SENIOR;
protected RankedEmployee() {}
public RankedEmployee(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
public Level getLevel() {
return level;
}
}
@@ -0,0 +1,26 @@
package lab.hibernate.model;
import jakarta.persistence.*;
/** SEQUENCE: ids are handed out in blocks, so inserts can be batched. */
@Entity
public class Receipt {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
public Long id;
public int number;
protected Receipt() {}
public Receipt(int number) {
this.number = number;
}
public Long getId() {
return id;
}
public int getNumber() {
return number;
}
}
@@ -0,0 +1,37 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import java.util.Objects;
/** hashCode built on the generated id: the classic trap. */
@Entity
public class SetItem {
@Id
@GeneratedValue
public Long id;
public String name;
protected SetItem() {}
public SetItem(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
return o instanceof SetItem other && Objects.equals(id, other.id);
}
@Override
public int hashCode() {
return Objects.hashCode(id);
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
@@ -0,0 +1,27 @@
package lab.hibernate.model;
import jakarta.persistence.*;
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "kind")
public abstract class Shape {
@Id
@GeneratedValue
public Long id;
public String label;
protected Shape() {}
protected Shape(String label) {
this.label = label;
}
public Long getId() {
return id;
}
public String getLabel() {
return label;
}
}
@@ -0,0 +1,19 @@
package lab.hibernate.model;
import jakarta.persistence.Entity;
@Entity
public class Square extends Shape {
public Integer side;
protected Square() {}
public Square(String label, int side) {
super(label);
this.side = side;
}
public Integer getSide() {
return side;
}
}
@@ -0,0 +1,42 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import java.util.ArrayList;
import java.util.List;
/** Owns a unidirectional one-to-many: the collection belongs to this entity, so it is versioned with it. */
@Entity
public class Wallet {
@Id
@GeneratedValue
public Long id;
public String owner;
@Version
public int version;
@OneToMany(cascade = CascadeType.ALL)
public List<Coin> coins = new ArrayList<>();
protected Wallet() {}
public Wallet(String owner) {
this.owner = owner;
}
public Long getId() {
return id;
}
public String getOwner() {
return owner;
}
public int getVersion() {
return version;
}
public List<Coin> getCoins() {
return coins;
}
}
@@ -0,0 +1,37 @@
package lab.hibernate.model;
import jakarta.persistence.*;
import org.hibernate.annotations.DynamicUpdate;
@Entity
@DynamicUpdate
public class Wide {
@Id
@GeneratedValue
public Long id;
public String a = "a";
public String b = "b";
public String c = "c";
protected Wide() {}
public Wide(String a) {
this.a = a;
}
public Long getId() {
return id;
}
public String getA() {
return a;
}
public String getB() {
return b;
}
public String getC() {
return c;
}
}
@@ -0,0 +1,207 @@
package lab.hibernate;
import static org.junit.jupiter.api.Assertions.*;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Set;
import java.util.TreeSet;
import lab.hibernate.model.*;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.junit.jupiter.api.Test;
class A_FundamentalsTest extends Lab {
@Test
void q01_jpaVersusHibernate() {
q(1, "jpa-vs-hibernate");
boot(Department.class, Employee.class, Project.class);
try (Session s = sf.openSession()) {
EntityManager em = s;
p("Session is an EntityManager : %s", s instanceof EntityManager);
p("SessionFactory is an EMF : %s", sf instanceof EntityManagerFactory);
p("em.unwrap(Session.class) == em : %s", em.unwrap(Session.class) == em);
p("Hibernate-only API reachable : s.getStatistics().isStatisticsEnabled() = %s", s.getSessionFactory().getStatistics().isStatisticsEnabled());
assertTrue(s instanceof EntityManager);
}
}
@Test
void q02_entityStates() {
q(2, "entity-states");
boot(Department.class, Employee.class, Project.class);
Employee e = new Employee("Asha", 100, null);
try (Session s = sf.openSession()) {
Transaction tx = s.beginTransaction();
p("new Employee() : managed=%s id=%s", s.contains(e), e.id);
s.persist(e);
p("after persist() : managed=%s id assigned=%s", s.contains(e), e.id != null);
tx.commit();
s.detach(e);
p("after detach() : managed=%s", s.contains(e));
Transaction tx2 = s.beginTransaction();
Employee managed = s.merge(e);
p("after merge() : original managed=%s, returned copy managed=%s, same object=%s", s.contains(e), s.contains(managed), e == managed);
s.remove(managed);
p("after remove() : managed=%s", s.contains(managed));
tx2.commit();
assertFalse(s.contains(managed));
}
}
@Test
void q03_persistVersusMerge() {
q(3, "persist-vs-merge");
boot(Department.class, Employee.class, Project.class);
Employee e = new Employee("Ben", 100, null);
tx(s -> s.persist(e));
// e is now detached (its session is closed)
try {
tx(s -> s.persist(e));
} catch (Exception ex) {
p("persist(detached) -> %s", first(ex));
}
Employee[] copy = new Employee[1];
tx(s -> {
copy[0] = s.merge(e);
e.salary = 777; // change the ORIGINAL after merge
p("merge(detached) -> returns a different instance: %s", copy[0] != e);
});
int salary = txr(s -> s.find(Employee.class, e.id).salary);
p("salary changed on the original after merge is stored: %s (db has %d)", salary == 777, salary);
assertEquals(100, salary);
}
@Test
void q04_saveUpdateRemovedInHibernate7() {
q(4, "save-update-removed");
Set<String> wanted = Set.of("save", "update", "saveOrUpdate", "delete", "load", "get", "persist", "merge", "remove", "find", "getReference", "refresh");
Set<String> present = new TreeSet<>();
for (Method m : Session.class.getMethods()) {
if (wanted.contains(m.getName())) {
present.add(m.getName());
}
}
Set<String> missing = new TreeSet<>(wanted);
missing.removeAll(present);
p("methods present on org.hibernate.Session : %s", present);
p("methods NOT present : %s", missing);
assertFalse(present.contains("save"));
assertFalse(present.contains("saveOrUpdate"));
assertFalse(present.contains("update"));
assertTrue(Arrays.stream(Session.class.getMethods()).anyMatch(m -> m.getName().equals("persist")));
}
@Test
void q05_findVersusGetReference() {
q(5, "find-vs-getreference");
boot(Department.class, Employee.class, Project.class);
Long id = txr(s -> {
Employee e = new Employee("Chen", 100, null);
s.persist(e);
return e.id;
});
try (Session s = sf.openSession()) {
int m = SqlLog.mark();
Employee ref = s.getReference(Employee.class, id);
p("getReference(): SQL statements so far = %d, initialized = %s", SqlLog.since(m).size(), Hibernate.isInitialized(ref));
String viaField = ref.name; // a public field is read straight off the proxy object
p("ref.name (field) -> %s; SQL statements now = %d, initialized = %s", viaField, SqlLog.since(m).size(), Hibernate.isInitialized(ref));
String name = ref.getName(); // a method call is intercepted and loads the row
p("ref.getName() -> %s; SQL statements now = %d, initialized = %s", name, SqlLog.since(m).size(), Hibernate.isInitialized(ref));
sql(m);
assertEquals(1, SqlLog.since(m).size());
}
try (Session s = sf.openSession()) {
p("find(missing id) -> %s", s.find(Employee.class, 99999L));
Employee ghost = s.getReference(Employee.class, 99999L);
p("getReference(missing) -> proxy returned without error: %s", ghost != null);
try {
ghost.getName();
} catch (RuntimeException ex) {
p("first access -> %s", first(ex));
}
}
}
@Test
void q06_dirtyCheckingAndFlush() {
q(6, "dirty-checking-flush");
boot(Department.class, Employee.class, Project.class);
Long id = txr(s -> {
Employee e = new Employee("Dev", 100, null);
s.persist(e);
return e.id;
});
int m = SqlLog.mark();
tx(s -> {
Employee e = s.find(Employee.class, id);
e.salary = 150; // no save(), no update()
});
p("change a managed entity, no save call:");
sql(m);
assertEquals(1, SqlLog.countSince(m, "update"));
int m2 = SqlLog.mark();
tx(s -> {
s.persist(new Employee("Eli", 90, null));
long n = s.createSelectionQuery("select count(e) from Employee e", Long.class).getSingleResult();
p("count visible inside the transaction (AUTO flush before the query): %d", n);
});
p("persist then query:");
sql(m2);
}
@Test
void q07_mergeVersusRefresh() {
q(7, "merge-vs-refresh");
boot(Department.class, Employee.class, Project.class);
Employee detached = txr(s -> {
Employee e = new Employee("Fay", 100, null);
s.persist(e);
return e;
});
detached.salary = 500;
tx(s -> {
Employee managed = s.find(Employee.class, detached.id);
managed.salary = 120; // pending change in memory
s.refresh(managed);
p("refresh(): pending in-memory change thrown away, salary = %d", managed.salary);
assertEquals(100, managed.salary);
});
tx(s -> s.merge(detached));
int now = txr(s -> s.find(Employee.class, detached.id).salary);
p("merge(detached with salary 500) -> database now has %d", now);
}
@Test
void q08_firstLevelCache() {
q(8, "first-level-cache");
boot(Department.class, Employee.class, Project.class);
Long id = txr(s -> {
Employee e = new Employee("Gus", 100, null);
s.persist(e);
return e.id;
});
try (Session s = sf.openSession()) {
int m = SqlLog.mark();
Employee a = s.find(Employee.class, id);
Employee b = s.find(Employee.class, id);
p("two find() calls, same session: SQL selects = %d, same instance = %s", SqlLog.countSince(m, "select"), a == b);
s.clear();
Employee c = s.find(Employee.class, id);
p("after clear(): SQL selects = %d, same instance as before = %s", SqlLog.countSince(m, "select"), a == c);
sql(m);
assertTrue(a == b);
}
try (Session s2 = sf.openSession()) {
int m = SqlLog.mark();
Employee d = s2.find(Employee.class, id);
p("a different session: SQL selects = %d (the first-level cache is per session)", SqlLog.countSince(m, "select"));
assertNotNull(d);
}
}
}
@@ -0,0 +1,148 @@
package lab.hibernate;
import static org.junit.jupiter.api.Assertions.*;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import lab.hibernate.model.*;
import org.hibernate.Session;
import org.junit.jupiter.api.Test;
class B_MappingTest extends Lab {
private List<String> tables() {
try (Session s = sf.openSession()) {
return s.createNativeQuery("select table_name from information_schema.tables where table_schema='public' order by 1", String.class).getResultList();
}
}
@Test
void q09_entityNeedsNoArgConstructor() {
q(9, "no-arg-constructor");
boot(BadEntity.class);
p("SessionFactory started with an entity that has no no-arg constructor: true");
try {
tx(s -> s.persist(new BadEntity(1L)));
p("persist(): ok");
tx(s -> s.find(BadEntity.class, 1L));
p("find(): ok");
} catch (RuntimeException ex) {
p("find() -> %s", first(ex));
Throwable root = ex;
while (root.getCause() != null) {
root = root.getCause();
}
p("root cause -> %s", first(root));
}
}
@Test
void q10_identityDisablesBatching() {
q(10, "identity-vs-sequence-batching");
boot(new String[] {"hibernate.jdbc.batch_size", "10"}, Invoice.class, Receipt.class);
int m = SqlLog.mark();
tx(s -> {
for (int i = 0; i < 25; i++) {
s.persist(new Receipt(i));
}
});
long seqInserts = SqlLog.countSince(m, "insert");
long seqSelects = SqlLog.countSince(m, "select");
p("SEQUENCE: 25 entities -> %d prepared insert statement(s) with jdbc.batch_size=10, %d sequence selects", seqInserts, seqSelects);
int m2 = SqlLog.mark();
tx(s -> {
for (int i = 0; i < 25; i++) {
s.persist(new Invoice(i));
}
});
long idInserts = SqlLog.countSince(m2, "insert");
p("IDENTITY: 25 entities -> %d prepared insert statements (one per row)", idInserts);
assertTrue(idInserts > seqInserts);
}
@Test
void q11_embeddableFlattensIntoOwnerTable() {
q(11, "embeddable-columns");
boot(Department.class, Employee.class, Project.class);
try (Session s = sf.openSession()) {
List<String> cols = s.createNativeQuery("select column_name from information_schema.columns where table_name='employee' order by 1", String.class).getResultList();
p("columns of table employee: %s", cols);
assertTrue(cols.contains("city") && cols.contains("zip"));
}
}
@Test
void q12_enumeratedOrdinalVersusString() {
q(12, "enum-ordinal-vs-string");
boot(Department.class, Employee.class, Project.class, RankedEmployee.class);
tx(s -> {
Employee e = new Employee("Hana", 1, null);
e.level = Level.LEAD;
s.persist(e);
RankedEmployee r = new RankedEmployee("Hana");
r.level = Level.LEAD;
s.persist(r);
});
try (Session s = sf.openSession()) {
Object str = s.createNativeQuery("select level from employee", Object.class).getSingleResult();
Object ord = s.createNativeQuery("select level from rankedemployee", Object.class).getSingleResult();
p("@Enumerated(STRING) stores LEAD as: %s", str);
p("@Enumerated (default) stores LEAD as: %s (its position in the enum: %s)", ord, java.util.Arrays.toString(Level.values()));
}
}
@Test
void q13_elementCollectionCreatesATable() {
q(13, "element-collection-table");
boot(Post.class, Comment.class);
p("tables: %s", tables());
assertTrue(tables().contains("post_tags"));
}
@Test
void q14_inheritanceStrategies() {
q(14, "inheritance-strategies");
boot(Payment.class, CardPayment.class, BankPayment.class, Shape.class, Circle.class, Square.class);
p("tables: %s", tables());
tx(s -> {
s.persist(new CardPayment(100, "4242"));
s.persist(new BankPayment(200, "DE00"));
s.persist(new Circle("c", 3));
s.persist(new Square("s", 4));
});
int m = SqlLog.mark();
tx(s -> {
p("JOINED 'from Payment' returned %d rows", s.createSelectionQuery("from Payment", Payment.class).getResultList().size());
p("SINGLE_TABLE 'from Shape' returned %d rows", s.createSelectionQuery("from Shape", Shape.class).getResultList().size());
});
sql(m);
}
@Test
void q15_mappedSuperclassIsNotQueryable() {
q(15, "mapped-superclass");
boot(Note.class);
tx(s -> s.persist(new Note("hello")));
p("tables: %s", tables());
tx(s -> p("select from the entity Note: %d row(s), created_by column inherited = %s", s.createSelectionQuery("from Note", Note.class).getResultList().size(), s.createSelectionQuery("from Note", Note.class).getSingleResult().createdBy));
try {
tx(s -> s.createSelectionQuery("from Auditable", Object.class).getResultList());
} catch (RuntimeException ex) {
p("select from the @MappedSuperclass -> %s", first(ex));
}
}
@Test
void q16_hashCodeOnGeneratedId() {
q(16, "equals-hashcode-generated-id");
boot(SetItem.class);
Set<SetItem> set = new HashSet<>();
SetItem item = new SetItem("x");
set.add(item);
p("before persist: id=%s, set.contains(item)=%s", item.id, set.contains(item));
tx(s -> s.persist(item));
p("after persist : id assigned=%s, hashCode changed, set.contains(item)=%s", item.id != null, set.contains(item));
assertFalse(set.contains(item));
}
}
@@ -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());
}
}
@@ -0,0 +1,100 @@
package lab.hibernate;
import static org.junit.jupiter.api.Assertions.*;
import java.util.List;
import lab.hibernate.model.Product;
import org.hibernate.stat.Statistics;
import org.junit.jupiter.api.Test;
class D_CachingTest extends Lab {
private static final String[] L2 = {
"hibernate.cache.use_second_level_cache", "true",
"hibernate.cache.use_query_cache", "true",
"hibernate.cache.region.factory_class", "jcache",
"hibernate.javax.cache.provider", "org.ehcache.jsr107.EhcacheCachingProvider",
"hibernate.javax.cache.missing_cache_strategy", "create"
};
private Long seed() {
boot(L2, Product.class);
return txr(s -> {
Product pr = new Product("Pen", 10);
s.persist(pr);
return pr.getId();
});
}
@Test
void q28_secondLevelCache() {
q(28, "second-level-cache");
Long id = seed();
Statistics st = sf.getStatistics();
st.clear();
int m0 = SqlLog.mark();
tx(s -> s.find(Product.class, id));
p("right after persist() the row is already in L2 (READ_WRITE caches on insert): SQL selects = %d, hits = %d", SqlLog.countSince(m0, "select"), st.getSecondLevelCacheHitCount());
sf.getCache().evictAllRegions();
st.clear();
int m = SqlLog.mark();
tx(s -> s.find(Product.class, id));
tx(s -> s.find(Product.class, id));
tx(s -> s.find(Product.class, id));
p("after evictAllRegions(), three find() calls, three different sessions:");
p(" SQL selects : %d", SqlLog.countSince(m, "select"));
p(" L2 cache hits : %d", st.getSecondLevelCacheHitCount());
p(" L2 cache misses : %d", st.getSecondLevelCacheMissCount());
p(" L2 cache puts : %d", st.getSecondLevelCachePutCount());
assertEquals(1, SqlLog.countSince(m, "select"));
}
@Test
void q29_queryCache() {
q(29, "query-cache");
seed();
Statistics st = sf.getStatistics();
st.clear();
Runnable run = () -> tx(s -> s.createSelectionQuery("from Product p where p.price > 5", Product.class).setCacheable(true).getResultList());
int m = SqlLog.mark();
run.run();
p("run 1: query cache hits=%d misses=%d puts=%d", st.getQueryCacheHitCount(), st.getQueryCacheMissCount(), st.getQueryCachePutCount());
run.run();
p("run 2: query cache hits=%d misses=%d puts=%d", st.getQueryCacheHitCount(), st.getQueryCacheMissCount(), st.getQueryCachePutCount());
p("SQL selects for Product after 2 runs: %d", SqlLog.countSince(m, "select"));
tx(s -> s.persist(new Product("Ink", 20))); // any write to the table invalidates cached results
run.run();
p("run 3 after inserting a Product: query cache hits=%d misses=%d", st.getQueryCacheHitCount(), st.getQueryCacheMissCount());
p("SQL selects for Product after 3 runs: %d", SqlLog.countSince(m, "select"));
assertEquals(1, st.getQueryCacheHitCount());
}
@Test
void q30_bulkUpdatesAndTheCache() {
q(30, "bulk-update-vs-l2");
Long id = seed();
tx(s -> s.find(Product.class, id)); // warm the cache
Statistics st = sf.getStatistics();
tx(s -> s.createMutationQuery("update Product set price = 11").executeUpdate());
st.clear();
int priceAfterHql = txr(s -> s.find(Product.class, id).getPrice());
p("after an HQL bulk update to 11 : find() sees %d (L2 hits=%d misses=%d)", priceAfterHql, st.getSecondLevelCacheHitCount(), st.getSecondLevelCacheMissCount());
tx(s -> s.createNativeMutationQuery("update product set price = 12").executeUpdate());
st.clear();
int priceAfterNative = txr(s -> s.find(Product.class, id).getPrice());
p("after a native bulk update to 12 : find() sees %d (L2 hits=%d misses=%d)", priceAfterNative, st.getSecondLevelCacheHitCount(), st.getSecondLevelCacheMissCount());
assertEquals(11, priceAfterHql);
tx(s -> s.doWork(c -> {
try (var stmt = c.createStatement()) {
stmt.executeUpdate("update product set price = 13");
}
}));
st.clear();
int priceAfterJdbc = txr(s -> s.find(Product.class, id).getPrice());
p("after raw JDBC update to 13 : find() sees %d (L2 hits=%d misses=%d) <- stale: Hibernate was not told", priceAfterJdbc, st.getSecondLevelCacheHitCount(), st.getSecondLevelCacheMissCount());
assertEquals(12, priceAfterJdbc);
}
}
@@ -0,0 +1,204 @@
package lab.hibernate;
import static org.junit.jupiter.api.Assertions.*;
import jakarta.persistence.LockModeType;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import lab.hibernate.model.*;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.junit.jupiter.api.Test;
class E_LockingTest extends Lab {
@Test
void q31_optimisticLocking() {
q(31, "optimistic-locking");
boot(Account.class);
Long id = txr(s -> {
Account a = new Account("ann", 100);
s.persist(a);
return a.getId();
});
try (Session a = sf.openSession(); Session b = sf.openSession()) {
Transaction ta = a.beginTransaction();
Transaction tb = b.beginTransaction();
Account fromA = a.find(Account.class, id);
Account fromB = b.find(Account.class, id);
p("both sessions read balance=%d version=%d", fromA.getBalance(), fromA.getVersion());
fromA.balance += 50;
ta.commit();
p("session A commits +50 -> version is now %d", fromA.getVersion());
fromB.balance += 25;
int m = SqlLog.mark();
try {
tb.commit();
} catch (RuntimeException ex) {
p("session B commits +25 -> %s", first(ex));
Throwable c = ex.getCause();
if (c != null) {
p(" cause: %s", first(c));
}
tb.rollback();
}
sql(m);
}
long finalBalance = txr(s -> s.find(Account.class, id).getBalance());
p("final balance: %d (B's stale write was rejected, A's +50 kept)", finalBalance);
assertEquals(150, finalBalance);
}
@Test
void q32_pessimisticLocking() throws Exception {
q(32, "pessimistic-locking");
boot(Account.class);
Long id = txr(s -> {
Account a = new Account("bob", 100);
s.persist(a);
return a.getId();
});
CountDownLatch locked = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
AtomicReference<String> holderSql = new AtomicReference<>();
Thread holder = new Thread(() -> {
try (Session s = sf.openSession()) {
Transaction t = s.beginTransaction();
int m = SqlLog.mark();
s.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE);
holderSql.set(SqlLog.since(m).get(0).replaceAll("\\s+", " "));
locked.countDown();
release.await();
t.commit();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
holder.start();
locked.await();
p("session 1: find(..., PESSIMISTIC_WRITE) ran: %s", holderSql.get());
try (Session s = sf.openSession()) {
Transaction t = s.beginTransaction();
try {
s.find(Account.class, id, LockModeType.PESSIMISTIC_WRITE, java.util.Map.of("jakarta.persistence.lock.timeout", 0));
p("session 2: got the lock (unexpected)");
} catch (RuntimeException ex) {
p("session 2 with lock.timeout=0 -> %s", first(ex));
Throwable c = ex.getCause();
if (c != null) {
p(" cause: %s", first(c));
}
}
t.rollback();
}
release.countDown();
holder.join();
}
@Test
void q33_lostUpdateWithoutAVersion() {
q(33, "lost-update");
boot(PlainAccount.class);
Long id = txr(s -> {
PlainAccount a = new PlainAccount("cy", 100);
s.persist(a);
return a.getId();
});
try (Session a = sf.openSession(); Session b = sf.openSession()) {
Transaction ta = a.beginTransaction();
Transaction tb = b.beginTransaction();
PlainAccount fromA = a.find(PlainAccount.class, id);
PlainAccount fromB = b.find(PlainAccount.class, id);
fromA.balance += 50; // deposit 50
ta.commit();
fromB.balance += 25; // deposit 25, computed from the stale 100
int m = SqlLog.mark();
tb.commit();
sql(m);
}
long balance = txr(s -> s.find(PlainAccount.class, id).getBalance());
p("two deposits (+50, +25) on 100 -> final balance %d, expected 175: A's deposit silently lost", balance);
assertEquals(125, balance);
}
@Test
void q34_isolationLevels() {
q(34, "isolation-levels");
boot(PlainAccount.class);
Long id = txr(s -> {
PlainAccount a = new PlainAccount("di", 100);
s.persist(a);
return a.getId();
});
try (Session reader = sf.openSession()) {
Object level = reader.createNativeQuery("show transaction_isolation", Object.class).getSingleResult();
p("PostgreSQL default isolation level: %s", level);
}
for (String level : new String[] {"read committed", "repeatable read"}) {
tx(s -> s.createNativeMutationQuery("update plainaccount set balance = 100").executeUpdate());
try (Session reader = sf.openSession()) {
Transaction t = reader.beginTransaction();
reader.createNativeMutationQuery("set transaction isolation level " + level).executeUpdate();
long first = reader.createNativeQuery("select balance from plainaccount where id = :id", Long.class).setParameter("id", id).getSingleResult();
tx(s -> s.createNativeMutationQuery("update plainaccount set balance = 150").executeUpdate());
long second = reader.createNativeQuery("select balance from plainaccount where id = :id", Long.class).setParameter("id", id).getSingleResult();
p("%-16s: first read %d, another transaction commits 150, second read %d -> %s", level, first, second, first == second ? "repeatable" : "non-repeatable read");
t.rollback();
}
}
}
@Test
void q35_versionBumpsOnOwnedCollectionOnly() {
q(35, "version-bump-collections");
boot(Wallet.class, Coin.class, Folder.class, Doc.class);
Long wid = txr(s -> {
Wallet w = new Wallet("eve");
s.persist(w);
return w.getId();
});
Long fid = txr(s -> {
Folder f = new Folder("inbox");
s.persist(f);
return f.getId();
});
int wBefore = txr(s -> s.find(Wallet.class, wid).getVersion());
tx(s -> s.find(Wallet.class, wid).getCoins().add(new Coin(25)));
int wAfter = txr(s -> s.find(Wallet.class, wid).getVersion());
p("Wallet (owns a unidirectional @OneToMany): add a Coin -> version %d -> %d", wBefore, wAfter);
int fBefore = txr(s -> s.find(Folder.class, fid).getVersion());
tx(s -> {
Folder f = s.find(Folder.class, fid);
Doc d = new Doc("a.txt", f);
f.getDocs().add(d);
});
int fAfter = txr(s -> s.find(Folder.class, fid).getVersion());
p("Folder (inverse, mappedBy @OneToMany) : add a Doc -> version %d -> %d", fBefore, fAfter);
assertEquals(wBefore + 1, wAfter);
assertEquals(fBefore, fAfter);
}
@Test
void q36_dynamicUpdate() {
q(36, "dynamic-update");
boot(Plain.class, Wide.class);
Long pid = txr(s -> {
Plain x = new Plain("one");
s.persist(x);
return x.getId();
});
Long wid = txr(s -> {
Wide x = new Wide("one");
s.persist(x);
return x.getId();
});
int m = SqlLog.mark();
tx(s -> s.find(Plain.class, pid).a = "two");
p("default entity, change one column:");
sql(m);
int m2 = SqlLog.mark();
tx(s -> s.find(Wide.class, wid).a = "two");
p("@DynamicUpdate entity, change one column:");
sql(m2);
}
}
@@ -0,0 +1,100 @@
package lab.hibernate;
import static org.junit.jupiter.api.Assertions.*;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
import java.util.List;
import lab.hibernate.model.*;
import org.hibernate.Session;
import org.hibernate.stat.Statistics;
import org.junit.jupiter.api.Test;
class F_QueryingTest extends Lab {
record EmployeeRow(String name, int salary) {}
private void seed() {
boot(Department.class, Employee.class, Project.class);
tx(s -> {
Department d = new Department("Eng");
s.persist(d);
s.persist(new Employee("Ana", 300, d));
s.persist(new Employee("Bo", 100, d));
s.persist(new Employee("Cy", 200, d));
});
}
@Test
void q37_hqlCriteriaNative() {
q(37, "hql-criteria-native");
seed();
int m = SqlLog.mark();
tx(s -> {
List<Employee> hql = s.createSelectionQuery("from Employee e where e.salary > :min order by e.salary", Employee.class).setParameter("min", 150).getResultList();
p("HQL -> %s", hql.stream().map(Employee::getName).toList());
CriteriaBuilder cb = sf.getCriteriaBuilder();
CriteriaQuery<Employee> cq = cb.createQuery(Employee.class);
Root<Employee> root = cq.from(Employee.class);
cq.select(root).where(cb.gt(root.get("salary"), 150)).orderBy(cb.asc(root.get("salary")));
List<Employee> crit = s.createSelectionQuery(cq).getResultList();
p("Criteria -> %s", crit.stream().map(Employee::getName).toList());
List<Employee> nat = s.createNativeQuery("select * from employee where salary > 150 order by salary", Employee.class).getResultList();
p("Native -> %s", nat.stream().map(Employee::getName).toList());
assertEquals(hql.size(), crit.size());
assertEquals(hql.size(), nat.size());
});
sql(m);
}
@Test
void q38_dtoProjection() {
q(38, "dto-projection");
seed();
Statistics st = sf.getStatistics();
st.clear();
int m = SqlLog.mark();
tx(s -> {
List<EmployeeRow> rows = s.createSelectionQuery("select e.name, e.salary from Employee e order by e.salary", EmployeeRow.class).getResultList();
p("record projection -> %s", rows);
});
p("entities loaded: %d", st.getEntityLoadCount());
sql(m);
st.clear();
tx(s -> s.createSelectionQuery("from Employee", Employee.class).getResultList());
p("full entity query loads %d entities", st.getEntityLoadCount());
assertEquals(3, st.getEntityLoadCount());
}
@Test
void q39_bulkUpdateBypassesPersistenceContext() {
q(39, "bulk-update-stale-entity");
seed();
tx(s -> {
Employee e = s.createSelectionQuery("from Employee where name = 'Bo'", Employee.class).getSingleResult();
int n = s.createMutationQuery("update Employee set salary = 999").executeUpdate();
p("HQL bulk update touched %d rows", n);
p("the entity already loaded still says salary = %d", e.salary);
s.refresh(e);
p("after refresh() it says salary = %d", e.salary);
assertEquals(999, e.salary);
});
}
@Test
void q40_namedQueriesAreValidatedAtStartup() {
q(40, "named-query-validation");
try {
boot(BrokenQuery.class);
p("SessionFactory started: true (unexpected)");
} catch (RuntimeException ex) {
p("buildSessionFactory() failed with %s:", ex.getClass().getName());
for (String line : String.valueOf(ex.getMessage()).strip().split("\n")) {
p(" %s", line.stripTrailing());
}
}
}
}
@@ -0,0 +1,94 @@
package lab.hibernate;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
import org.hibernate.Session;
import org.junit.jupiter.api.AfterEach;
/** Base class: boots a fresh schema on the local PostgreSQL per test and records a transcript file. */
abstract class Lab {
static final String URL = System.getProperty("lab.db.url", "jdbc:postgresql://localhost:5433/lab");
SessionFactory sf;
private final List<String> lines = new ArrayList<>();
private String file;
void q(int n, String slug) {
file = String.format("q%02d-%s.txt", n, slug);
}
void p(String format, Object... args) {
lines.add(args.length == 0 ? format : String.format(format, args));
}
/** Print the SQL recorded since the mark, one statement per line. */
void sql(int mark) {
for (String s : SqlLog.since(mark)) {
lines.add("SQL> " + s.replaceAll("\\s+", " ").strip());
}
}
Configuration config(String... extra) {
Configuration cfg = new Configuration();
cfg.setProperty("jakarta.persistence.jdbc.url", URL);
cfg.setProperty("jakarta.persistence.jdbc.user", "postgres");
cfg.setProperty("hibernate.hbm2ddl.auto", "create-drop");
cfg.setProperty("hibernate.generate_statistics", "true");
cfg.setProperty("hibernate.session_factory.statement_inspector", SqlLog.class.getName());
for (int i = 0; i < extra.length; i += 2) {
cfg.setProperty(extra[i], extra[i + 1]);
}
return cfg;
}
void boot(Class<?>... entities) {
boot(new String[0], entities);
}
void boot(String[] extra, Class<?>... entities) {
Configuration cfg = config(extra);
for (Class<?> c : entities) {
cfg.addAnnotatedClass(c);
}
sf = cfg.buildSessionFactory();
}
void tx(Consumer<Session> work) {
sf.inTransaction(work);
}
<T> T txr(Function<Session, T> work) {
return sf.fromTransaction(work);
}
@AfterEach
void finish() {
if (sf != null && sf.isOpen()) {
sf.close();
}
if (file == null) {
return;
}
try {
Path dir = Path.of("output");
Files.createDirectories(dir);
Files.writeString(dir.resolve(file), String.join("\n", lines) + "\n");
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
static String first(Throwable t) {
String m = String.valueOf(t.getMessage()).strip();
int nl = m.indexOf('\n');
return t.getClass().getName() + ": " + (nl > 0 ? m.substring(0, nl) : m);
}
}