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:
asmhatre
2026-11-14 01:34:34 +05:30
co-authored by Claude Sonnet 5.5
commit 453fad0ee7
29 changed files with 1109 additions and 0 deletions
@@ -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);
}
}
}
}
}
+95
View File
@@ -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);
}
}
}