Hibernate 8 + Jakarta Persistence 4: EntityAgent and stateless sessions demo
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b7jUQrypvpE1ix1k7V
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** EntityManager tracks changes; EntityAgent does not. Same code, different outcome. */
|
||||
class Demo01Test {
|
||||
|
||||
@Test
|
||||
void sameEditTwoHandlers() {
|
||||
try (Lab lab = new Lab(); Transcript t = new Transcript("01-entitymanager-vs-entityagent")) {
|
||||
lab.seed();
|
||||
|
||||
// 1. EntityManager: load, change a field, commit. Nobody calls update().
|
||||
lab.sf.runInTransaction(em -> {
|
||||
Book b = em.find(Book.class, "978-1");
|
||||
b.title = "Changed via EntityManager";
|
||||
});
|
||||
t.sql("EntityManager: find, edit title, commit", lab.sql());
|
||||
assertEquals(2, lab.sql().size(), "select + automatic dirty-check update");
|
||||
assertTrue(lab.sql().get(1).contains("update book"));
|
||||
String afterEm = lab.sf.callInTransaction(EntityAgent.class, a -> a.get(Book.class, "978-1")).title;
|
||||
t.line("title in database afterwards: %s", afterEm);
|
||||
lab.resetSql();
|
||||
|
||||
// 2. EntityAgent: the same three steps.
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book b = a.get(Book.class, "978-2");
|
||||
b.title = "Changed via EntityAgent";
|
||||
});
|
||||
t.line("");
|
||||
t.sql("EntityAgent: get, edit title, commit", lab.sql());
|
||||
assertEquals(1, lab.sql().size(), "select only: nothing noticed the edit");
|
||||
String afterAgent = lab.sf.callInTransaction(EntityAgent.class, a -> a.get(Book.class, "978-2")).title;
|
||||
t.line("title in database afterwards: %s", afterAgent);
|
||||
assertEquals("Book 2", afterAgent, "the edit was silently lost");
|
||||
lab.resetSql();
|
||||
|
||||
// 3. EntityAgent with the explicit update() call.
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book b = a.get(Book.class, "978-2");
|
||||
b.title = "Changed via EntityAgent + update()";
|
||||
a.update(b);
|
||||
});
|
||||
t.line("");
|
||||
t.sql("EntityAgent: get, edit title, update(book), commit", lab.sql());
|
||||
assertEquals(2, lab.sql().size());
|
||||
String afterUpdate = lab.sf.callInTransaction(EntityAgent.class, a -> a.get(Book.class, "978-2")).title;
|
||||
t.line("title in database afterwards: %s", afterUpdate);
|
||||
assertEquals("Changed via EntityAgent + update()", afterUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertSame;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/** get() vs find(), and the missing first-level cache. */
|
||||
class Demo02Test {
|
||||
|
||||
@Test
|
||||
void getFindAndNoCache() {
|
||||
try (Lab lab = new Lab(); Transcript t = new Transcript("02-get-find-and-no-cache")) {
|
||||
lab.seed();
|
||||
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book missing = a.find(Book.class, "no-such-isbn");
|
||||
t.line("agent.find(missing) -> %s", missing);
|
||||
assertNull(missing);
|
||||
|
||||
EntityNotFoundException e = assertThrows(EntityNotFoundException.class,
|
||||
() -> a.get(Book.class, "no-such-isbn"));
|
||||
t.line("agent.get(missing) -> %s", e.getClass().getSimpleName());
|
||||
});
|
||||
|
||||
lab.resetSql();
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book first = a.get(Book.class, "978-1");
|
||||
Book second = a.get(Book.class, "978-1");
|
||||
t.line("");
|
||||
t.line("EntityAgent, same id twice in one transaction:");
|
||||
t.line(" same instance? %s", first == second);
|
||||
assertNotSame(first, second);
|
||||
});
|
||||
t.sql(" SQL", lab.sql());
|
||||
assertEquals(2, lab.sql().size(), "no first-level cache: two selects");
|
||||
|
||||
lab.resetSql();
|
||||
lab.sf.runInTransaction(em -> {
|
||||
Book first = em.find(Book.class, "978-1");
|
||||
Book second = em.find(Book.class, "978-1");
|
||||
t.line("");
|
||||
t.line("EntityManager, same id twice in one transaction:");
|
||||
t.line(" same instance? %s", first == second);
|
||||
assertSame(first, second);
|
||||
});
|
||||
t.sql(" SQL", lab.sql());
|
||||
assertEquals(1, lab.sql().size(), "persistence context answers the second find");
|
||||
|
||||
lab.resetSql();
|
||||
List<Book> books = lab.sf.callInTransaction(EntityAgent.class,
|
||||
a -> a.getMultiple(Book.class, List.of("978-1", "978-2", "978-3")));
|
||||
t.line("");
|
||||
t.line("agent.getMultiple(3 ids) returned %d books", books.size());
|
||||
t.sql(" SQL", lab.sql());
|
||||
assertEquals(3, books.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** Lazy associations are not loaded for you; fetch() loads them on request. */
|
||||
class Demo03Test {
|
||||
|
||||
@Test
|
||||
void lazyAssociationsNeedFetch() {
|
||||
try (Lab lab = new Lab(); Transcript t = new Transcript("03-lazy-associations-and-fetch")) {
|
||||
lab.seed();
|
||||
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book b = a.get(Book.class, "978-1");
|
||||
boolean init = Hibernate.isInitialized(b.publisher);
|
||||
t.line("agent.get(Book): publisher initialized? %s", init);
|
||||
assertFalse(init);
|
||||
|
||||
Object viaGetter;
|
||||
try {
|
||||
viaGetter = b.publisher.getName();
|
||||
} catch (RuntimeException e) {
|
||||
viaGetter = e.getClass().getSimpleName();
|
||||
}
|
||||
t.line("book.publisher.getName() before fetch -> %s", viaGetter);
|
||||
|
||||
lab.resetSql();
|
||||
Publisher p = a.fetch(b.publisher);
|
||||
t.line("agent.fetch(book.publisher) -> getName() = %s", p.getName());
|
||||
t.line(" same object as book.publisher? %s, initialized now? %s",
|
||||
p == b.publisher, Hibernate.isInitialized(p));
|
||||
t.line(" reading the public field p.name instead -> %s", p.name);
|
||||
t.sql(" SQL", lab.sql());
|
||||
assertEquals("Manning", p.getName());
|
||||
assertEquals(1, lab.sql().size());
|
||||
});
|
||||
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Publisher p = a.get(Publisher.class, lab.sf.callInTransaction(em ->
|
||||
em.createQuery("select p.id from Publisher p", Long.class).getSingleResult()));
|
||||
boolean init = Hibernate.isInitialized(p.books);
|
||||
t.line("");
|
||||
t.line("agent.get(Publisher): books collection initialized? %s", init);
|
||||
assertFalse(init);
|
||||
|
||||
lab.resetSql();
|
||||
var books = a.fetch(p.books);
|
||||
t.line("agent.fetch(publisher.books) -> %d books", books.size());
|
||||
t.sql(" SQL", lab.sql());
|
||||
assertEquals(3, books.size());
|
||||
assertTrue(Hibernate.isInitialized(books));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import org.hibernate.Session;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/** Bulk insert: what the persistence context costs, and what insert() returns. */
|
||||
class Demo04Test {
|
||||
|
||||
private static final int N = 5_000;
|
||||
|
||||
@Test
|
||||
void bulkInsert() {
|
||||
try (Lab lab = new Lab(50); Transcript t = new Transcript("04-bulk-insert")) {
|
||||
var stats = lab.sf.getStatistics();
|
||||
|
||||
// EntityManager, naive loop with no flush()/clear().
|
||||
stats.clear();
|
||||
int[] managed = new int[1];
|
||||
lab.sf.runInTransaction(em -> {
|
||||
for (int i = 0; i < N; i++) {
|
||||
em.persist(new Book("em-" + i, "Title " + i, "9.99"));
|
||||
}
|
||||
managed[0] = em.unwrap(Session.class).getStatistics().getEntityCount();
|
||||
});
|
||||
t.line("EntityManager.persist x %d in one transaction", N);
|
||||
t.line(" entities held in the persistence context at the end: %d", managed[0]);
|
||||
t.line(" entity inserts: %d statements prepared: %d",
|
||||
stats.getEntityInsertCount(), stats.getPrepareStatementCount());
|
||||
assertEquals(N, managed[0]);
|
||||
|
||||
// EntityAgent, one insert() per row.
|
||||
stats.clear();
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
for (int i = 0; i < N; i++) {
|
||||
a.insert(new Book("ag-" + i, "Title " + i, "9.99"));
|
||||
}
|
||||
});
|
||||
t.line("");
|
||||
t.line("EntityAgent.insert x %d in one transaction", N);
|
||||
t.line(" entities held anywhere: 0 (a StatelessSession has no persistence context)");
|
||||
t.line(" entity inserts: %d statements prepared: %d",
|
||||
stats.getEntityInsertCount(), stats.getPrepareStatementCount());
|
||||
assertEquals(N, stats.getEntityInsertCount());
|
||||
|
||||
// EntityAgent, insertMultiple().
|
||||
stats.clear();
|
||||
List<Book> batch = new ArrayList<>();
|
||||
for (int i = 0; i < N; i++) {
|
||||
batch.add(new Book("mu-" + i, "Title " + i, "9.99"));
|
||||
}
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> a.insertMultiple(batch));
|
||||
t.line("");
|
||||
t.line("EntityAgent.insertMultiple(list of %d)", N);
|
||||
t.line(" entity inserts: %d statements prepared: %d",
|
||||
stats.getEntityInsertCount(), stats.getPrepareStatementCount());
|
||||
assertEquals(N, stats.getEntityInsertCount());
|
||||
|
||||
// What insert() hands back: nothing. The generated id lands on the entity instead.
|
||||
Publisher p = new Publisher("Generated id demo");
|
||||
t.line("");
|
||||
t.line("publisher.id before insert: %s", p.id);
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> a.insert(p));
|
||||
t.line("publisher.id after insert: %s (insert() itself returns void)", p.id);
|
||||
assertNotNull(p.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import jakarta.persistence.LockModeType;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** Lock modes on a stateless handler: when does the lock (or version check) actually reach the database? */
|
||||
class Demo05Test {
|
||||
|
||||
private static int version(Lab lab, String isbn) {
|
||||
int v = lab.sf.callInTransaction(EntityAgent.class, a -> a.get(Book.class, isbn)).version;
|
||||
lab.resetSql();
|
||||
return v;
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockModesInStatelessHandler() {
|
||||
try (Transcript t = new Transcript("05-lock-modes-in-entityagent")) {
|
||||
for (Lab.Db db : Lab.Db.values()) {
|
||||
try (Lab lab = new Lab(db)) {
|
||||
lab.seed();
|
||||
t.line("=== %s ===", db);
|
||||
t.line("");
|
||||
|
||||
for (LockModeType mode : List.of(LockModeType.OPTIMISTIC, LockModeType.OPTIMISTIC_FORCE_INCREMENT,
|
||||
LockModeType.PESSIMISTIC_WRITE, LockModeType.PESSIMISTIC_FORCE_INCREMENT)) {
|
||||
int before = version(lab, "978-1");
|
||||
List<String> afterGet = new ArrayList<>();
|
||||
List<String> afterCommit = new ArrayList<>();
|
||||
|
||||
String failure = null;
|
||||
try (EntityAgent a = lab.sf.createEntityAgent()) {
|
||||
var tx = a.getTransaction();
|
||||
tx.begin();
|
||||
try {
|
||||
a.get(Book.class, "978-1", mode);
|
||||
afterGet.addAll(lab.sql());
|
||||
lab.resetSql();
|
||||
tx.commit();
|
||||
afterCommit.addAll(lab.sql());
|
||||
} catch (RuntimeException e) {
|
||||
failure = e.getClass().getName() + ": " + e.getMessage();
|
||||
if (tx.isActive()) {
|
||||
tx.rollback();
|
||||
}
|
||||
}
|
||||
}
|
||||
lab.resetSql();
|
||||
int after = version(lab, "978-1");
|
||||
|
||||
t.line("get(Book.class, \"978-1\", %s)", mode);
|
||||
if (failure != null) {
|
||||
t.line(" FAILED: %s", failure);
|
||||
t.line("");
|
||||
if (db == Lab.Db.HSQLDB && mode == LockModeType.OPTIMISTIC) {
|
||||
assertTrue(failure.contains("Stateless optimistic locking requires transaction-long row protection"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
t.sql(" right after get()", afterGet);
|
||||
t.sql(" during commit", afterCommit);
|
||||
t.line(" version before: %d, after: %d", before, after);
|
||||
t.line("");
|
||||
|
||||
switch (mode) {
|
||||
case OPTIMISTIC, PESSIMISTIC_WRITE -> assertEquals(before, after);
|
||||
case OPTIMISTIC_FORCE_INCREMENT, PESSIMISTIC_FORCE_INCREMENT ->
|
||||
assertEquals(before + 1, after);
|
||||
default -> { }
|
||||
}
|
||||
assertEquals(0, afterCommit.size(), "nothing is deferred to commit");
|
||||
if (db == Lab.Db.H2 && mode == LockModeType.OPTIMISTIC) {
|
||||
assertTrue(afterGet.get(0).endsWith("for update"),
|
||||
"pinned to CR1: OPTIMISTIC reads with a row lock on H2");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
/** @ExcludedFromVersioning: a field that may change without bumping the optimistic version. */
|
||||
class Demo06Test {
|
||||
|
||||
@Test
|
||||
void excludedFromVersioning() {
|
||||
try (Lab lab = new Lab(); Transcript t = new Transcript("06-excluded-from-versioning")) {
|
||||
lab.seed();
|
||||
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book b = a.get(Book.class, "978-1");
|
||||
t.line("loaded: version=%d views=%d", b.version, b.views);
|
||||
b.views = 42;
|
||||
a.update(b);
|
||||
});
|
||||
Book afterViews = lab.sf.callInTransaction(EntityAgent.class, a -> a.get(Book.class, "978-1"));
|
||||
t.line("after update() that changed only views: version=%d views=%d", afterViews.version, afterViews.views);
|
||||
assertEquals(0, afterViews.version);
|
||||
assertEquals(42, afterViews.views);
|
||||
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book b = a.get(Book.class, "978-1");
|
||||
b.title = "Retitled";
|
||||
a.update(b);
|
||||
});
|
||||
Book afterTitle = lab.sf.callInTransaction(EntityAgent.class, a -> a.get(Book.class, "978-1"));
|
||||
t.line("after update() that changed title: version=%d views=%d", afterTitle.version, afterTitle.views);
|
||||
assertEquals(1, afterTitle.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import jakarta.persistence.Tuple;
|
||||
import jakarta.persistence.sql.ResultSetMapping;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
import static jakarta.persistence.sql.ResultSetMapping.column;
|
||||
import static jakarta.persistence.sql.ResultSetMapping.constructor;
|
||||
import static jakarta.persistence.sql.ResultSetMapping.entity;
|
||||
import static jakarta.persistence.sql.ResultSetMapping.field;
|
||||
import static jakarta.persistence.sql.ResultSetMapping.tuple;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
|
||||
/** Programmatic result set mappings: entity, constructor and tuple, with no @SqlResultSetMapping. */
|
||||
class Demo07Test {
|
||||
|
||||
@Test
|
||||
void programmaticResultSetMappings() {
|
||||
try (Lab lab = new Lab(); Transcript t = new Transcript("07-programmatic-result-set-mapping")) {
|
||||
lab.seed();
|
||||
String sql = "select isbn, title as book_title, price, version, views, publisher_id from book order by isbn";
|
||||
|
||||
ResultSetMapping<Book> asEntity = entity(Book.class,
|
||||
field(Book.class, String.class, "isbn", "isbn"),
|
||||
field(Book.class, String.class, "title", "book_title"),
|
||||
field(Book.class, BigDecimal.class, "price", "price"),
|
||||
field(Book.class, Integer.class, "version", "version"),
|
||||
field(Book.class, Integer.class, "views", "views"));
|
||||
|
||||
List<Book> viaAgent = lab.sf.callInTransaction(EntityAgent.class,
|
||||
a -> a.createNativeQuery(sql, asEntity).getResultList());
|
||||
t.line("entity mapping through EntityAgent: %d rows, first = %s / %s / %s",
|
||||
viaAgent.size(), viaAgent.get(0).isbn, viaAgent.get(0).title, viaAgent.get(0).price);
|
||||
assertEquals(3, viaAgent.size());
|
||||
|
||||
lab.sf.runInTransaction(em -> {
|
||||
List<Book> viaEm = em.createNativeQuery(sql, asEntity).getResultList();
|
||||
t.line("entity mapping through EntityManager: %d rows, managed by the persistence context? %s",
|
||||
viaEm.size(), em.contains(viaEm.get(0)));
|
||||
});
|
||||
|
||||
// A constructor mapping needs no entity at all.
|
||||
List<BookSummary> summaries = lab.sf.callInTransaction(EntityAgent.class,
|
||||
a -> a.createNativeQuery("select title, price from book order by isbn",
|
||||
constructor(BookSummary.class,
|
||||
column("title", String.class),
|
||||
column("price", BigDecimal.class))).getResultList());
|
||||
t.line("");
|
||||
t.line("constructor mapping -> %s", summaries.get(0));
|
||||
assertEquals("Book 1", summaries.get(0).title());
|
||||
|
||||
// A tuple mapping, read back by column position.
|
||||
List<Tuple> tuples = lab.sf.callInTransaction(EntityAgent.class,
|
||||
a -> a.createNativeQuery("select isbn, views from book order by isbn",
|
||||
tuple(column("isbn", String.class), column("views", Integer.class))).getResultList());
|
||||
Tuple first = tuples.get(0);
|
||||
t.line("tuple mapping -> (%s, %s)", first.get(0, String.class), first.get(1, Integer.class));
|
||||
assertFalse(tuples.isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import jakarta.persistence.EntityExistsException;
|
||||
import org.hibernate.StatelessSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/** upsert on an unversioned entity, delete, the duplicate-key exception, and what an EntityAgent really is. */
|
||||
class Demo08Test {
|
||||
|
||||
@Test
|
||||
void upsertDeleteAndErrors() {
|
||||
try (Lab lab = new Lab(); Transcript t = new Transcript("08-upsert-delete-and-errors")) {
|
||||
lab.seed();
|
||||
|
||||
try (EntityAgent a = lab.sf.createEntityAgent()) {
|
||||
t.line("createEntityAgent() returns: %s", a.getClass().getName());
|
||||
t.line("instanceof org.hibernate.StatelessSession: %s", a instanceof StatelessSession);
|
||||
assertTrue(a instanceof StatelessSession);
|
||||
}
|
||||
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> a.insert(new Setting("theme", "light")));
|
||||
lab.resetSql();
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
a.upsert(new Setting("theme", "dark")); // row exists
|
||||
a.upsert(new Setting("language", "en")); // row does not exist
|
||||
});
|
||||
t.line("");
|
||||
t.sql("upsert(theme=dark) on an existing row, upsert(language=en) on a new row", lab.sql());
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
t.line("theme -> %s", a.get(Setting.class, "theme").val);
|
||||
t.line("language -> %s", a.get(Setting.class, "language").val);
|
||||
assertEquals("dark", a.get(Setting.class, "theme").val);
|
||||
assertEquals("en", a.get(Setting.class, "language").val);
|
||||
});
|
||||
|
||||
t.line("");
|
||||
EntityExistsException dup = assertThrows(EntityExistsException.class,
|
||||
() -> lab.sf.runInTransaction(EntityAgent.class,
|
||||
a -> a.insert(new Book("978-2", "Duplicate", "1.00"))));
|
||||
t.line("insert() with an existing primary key -> %s", dup.getClass().getName());
|
||||
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book b = a.get(Book.class, "978-3");
|
||||
a.delete(b);
|
||||
});
|
||||
Book gone = lab.sf.callInTransaction(EntityAgent.class, a -> a.find(Book.class, "978-3"));
|
||||
t.line("find() after delete() -> %s", gone);
|
||||
assertNull(gone);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import jakarta.persistence.EntityAgent;
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
/**
|
||||
* Same stateless operations, two databases. These assertions are deliberately PINNED to
|
||||
* Hibernate 8.0.0.CR1: if a later build changes what H2 throws, this test fails and the blog post
|
||||
* needs correcting.
|
||||
*/
|
||||
class Demo09Test {
|
||||
|
||||
private static String attempt(Runnable r) {
|
||||
try {
|
||||
r.run();
|
||||
return "no exception";
|
||||
} catch (RuntimeException e) {
|
||||
// Only a bare HibernateException needs its message to be understood.
|
||||
return e.getClass() == org.hibernate.HibernateException.class
|
||||
? e.getClass().getName() + ": " + e.getMessage()
|
||||
: e.getClass().getName();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameOperationsTwoDatabases() {
|
||||
try (Transcript t = new Transcript("09-same-operations-two-databases")) {
|
||||
for (Lab.Db db : Lab.Db.values()) {
|
||||
try (Lab lab = new Lab(db)) {
|
||||
lab.seed();
|
||||
t.line("=== %s ===", db);
|
||||
|
||||
// A stale update(): another handler changes the row after we read it.
|
||||
Book stale = lab.sf.callInTransaction(EntityAgent.class, a -> a.get(Book.class, "978-1"));
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book winner = a.get(Book.class, "978-1");
|
||||
winner.title = "Winner";
|
||||
a.update(winner);
|
||||
});
|
||||
stale.title = "Loser";
|
||||
String agentStale = attempt(() ->
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> a.update(stale)));
|
||||
t.line("EntityAgent.update(stale book) -> %s", agentStale);
|
||||
|
||||
// The same conflict through the stateful API.
|
||||
Book stale2 = lab.sf.callInTransaction(em -> em.find(Book.class, "978-2"));
|
||||
lab.sf.runInTransaction(EntityAgent.class, a -> {
|
||||
Book winner = a.get(Book.class, "978-2");
|
||||
winner.title = "Winner";
|
||||
a.update(winner);
|
||||
});
|
||||
String emStale = attempt(() -> lab.sf.runInTransaction(em -> {
|
||||
Book managed = em.merge(stale2);
|
||||
managed.title = "Loser";
|
||||
}));
|
||||
t.line("EntityManager.merge(stale book) -> %s", emStale);
|
||||
|
||||
String ghost = attempt(() -> lab.sf.runInTransaction(EntityAgent.class,
|
||||
a -> a.update(new Book("no-such-isbn", "Ghost", "1.00"))));
|
||||
t.line("EntityAgent.update(row not in db) -> %s", ghost);
|
||||
|
||||
String upsertNew = attempt(() -> lab.sf.runInTransaction(EntityAgent.class,
|
||||
a -> a.upsert(new Book("978-9", "Brand new", "5.00"))));
|
||||
t.line("EntityAgent.upsert(new versioned) -> %s", upsertNew);
|
||||
t.line("");
|
||||
|
||||
assertEquals(OptimisticLockException.class.getName(), emStale);
|
||||
assertNotNull(agentStale);
|
||||
switch (db) {
|
||||
case H2 -> assertEquals("org.hibernate.HibernateException: The database returned no natively generated values : com.ankurm.agent.Book", agentStale);
|
||||
case HSQLDB -> assertEquals(OptimisticLockException.class.getName(), agentStale);
|
||||
}
|
||||
assertInstanceOf(String.class, ghost);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.jpa.HibernatePersistenceConfiguration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Boots a Hibernate 8 SessionFactory on an in-memory database and records every SQL statement
|
||||
* Hibernate prepares, so each demo can print exactly what reached the database.
|
||||
*/
|
||||
final class Lab implements AutoCloseable {
|
||||
|
||||
enum Db {
|
||||
H2("jdbc:h2:mem:lab-%s;DB_CLOSE_DELAY=-1"),
|
||||
HSQLDB("jdbc:hsqldb:mem:lab%s");
|
||||
|
||||
final String urlPattern;
|
||||
|
||||
Db(String urlPattern) {
|
||||
this.urlPattern = urlPattern;
|
||||
}
|
||||
|
||||
String url() {
|
||||
String id = UUID.randomUUID().toString().replace("-", "").substring(0, 12);
|
||||
return urlPattern.formatted(id);
|
||||
}
|
||||
}
|
||||
|
||||
final SessionFactory sf;
|
||||
final Db db;
|
||||
private final List<String> sql = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
Lab() {
|
||||
this(Db.H2, 50);
|
||||
}
|
||||
|
||||
Lab(int jdbcBatchSize) {
|
||||
this(Db.H2, jdbcBatchSize);
|
||||
}
|
||||
|
||||
Lab(Db db) {
|
||||
this(db, 50);
|
||||
}
|
||||
|
||||
Lab(Db db, int jdbcBatchSize) {
|
||||
this.db = db;
|
||||
this.sf = new HibernatePersistenceConfiguration("lab")
|
||||
.managedClasses(Book.class, Publisher.class, Setting.class)
|
||||
.jdbcUrl(db.url())
|
||||
.jdbcCredentials("sa", "")
|
||||
.property("jakarta.persistence.schema-generation.database.action", "create")
|
||||
.property("hibernate.jdbc.batch_size", String.valueOf(jdbcBatchSize))
|
||||
.collectStatistics(true)
|
||||
.statementInspector(s -> {
|
||||
sql.add(s);
|
||||
return s;
|
||||
})
|
||||
.createEntityManagerFactory();
|
||||
}
|
||||
|
||||
/** Statements prepared since the last {@link #resetSql()}. */
|
||||
List<String> sql() {
|
||||
synchronized (sql) {
|
||||
return List.copyOf(sql);
|
||||
}
|
||||
}
|
||||
|
||||
void resetSql() {
|
||||
sql.clear();
|
||||
}
|
||||
|
||||
/** Inserts one publisher and three books, then forgets the SQL this produced. */
|
||||
Publisher seed() {
|
||||
Publisher p = new Publisher("Manning");
|
||||
sf.runInTransaction(em -> {
|
||||
em.persist(p);
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
Book b = new Book("978-" + i, "Book " + i, "10.00");
|
||||
b.publisher = p;
|
||||
em.persist(b);
|
||||
}
|
||||
});
|
||||
resetSql();
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
sf.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.ankurm.agent;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Collects printed lines and writes them to output/NN-name.txt. Every figure in the blog post comes
|
||||
* from one of these files, and the same test asserts the same figure, so a number that stops being
|
||||
* true fails the build instead of silently going stale.
|
||||
*/
|
||||
final class Transcript implements AutoCloseable {
|
||||
|
||||
private final Path file;
|
||||
private final List<String> lines = new ArrayList<>();
|
||||
|
||||
Transcript(String name) {
|
||||
this.file = Path.of("output", name + ".txt");
|
||||
}
|
||||
|
||||
void line(String fmt, Object... args) {
|
||||
lines.add(args.length == 0 ? fmt : String.format(fmt, args));
|
||||
}
|
||||
|
||||
void sql(String label, List<String> statements) {
|
||||
line("%s (%d statement%s)", label, statements.size(), statements.size() == 1 ? "" : "s");
|
||||
for (String s : statements) {
|
||||
line(" %s", s);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
Files.createDirectories(file.getParent());
|
||||
Files.write(file, lines);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user