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:
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
target/
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
employee.department_id after d.employees.add(e) only: null
|
||||
after setting employee.department too : 1
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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>
|
||||
Executable
+9
@@ -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/"
|
||||
Executable
+12
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user