Add migration-5-to-7 module: Hibernate 5.6.15 vs 7.4.14 jar diff, compile checks, schema defaults and cheat-sheet snippets

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YPgGH6aMbSLANL3E2HHmS6
This commit is contained in:
2026-10-25 19:17:24 +00:00
co-authored by Claude Sonnet 5.5
parent 357ba84e5c
commit aea104f021
57 changed files with 5187 additions and 0 deletions
@@ -0,0 +1,8 @@
package com.ankurm.migration;
import jakarta.persistence.Embeddable;
/** A Java record used directly as an embeddable. */
@Embeddable
public record Address(String street, String city) {
}
@@ -0,0 +1,26 @@
package com.ankurm.migration;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.NaturalId;
@Entity
public class Author {
@Id
@GeneratedValue
Long id;
@NaturalId
String email;
String name;
public Author() {
}
Author(String email, String name) {
this.email = email;
this.name = name;
}
}
@@ -0,0 +1,29 @@
package com.ankurm.migration;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
@Entity
public class Book {
@Id
@GeneratedValue
Long id;
String title;
Integer pages;
@ManyToOne(fetch = FetchType.LAZY)
Author author;
public Book() {
}
Book(String title, Integer pages, Author author) {
this.title = title;
this.pages = pages;
this.author = author;
}
}
@@ -0,0 +1,217 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.StatelessSession;
import org.junit.jupiter.api.Test;
/** Snippets for the cheat sheet. Each test prints what it did to ../output/1N-cheat-*.txt and asserts the same values. */
class CheatSheetTest {
@Test
void sessionApi() {
try (Db db = new Db(Author.class, Book.class); Transcript t = new Transcript("10-cheat-session-api")) {
Long id;
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Author a = new Author("[email protected]", "Ann");
s.persist(a);
id = a.id;
s.getTransaction().commit();
t.line("persist(a) -> id assigned on the object: " + id);
}
try (Session s = db.sessionFactory.openSession()) {
db.clear();
Author found = s.find(Author.class, id);
t.line("find(Author, id) -> " + found.name + " (" + db.sql.size() + " select)");
assertThat(found.name).isEqualTo("Ann");
assertThat(db.sql).hasSize(1);
}
try (Session s = db.sessionFactory.openSession()) {
db.clear();
Author ref = s.getReference(Author.class, id);
t.line("getReference(Author, id) -> initialized: " + Hibernate.isInitialized(ref) + " (" + db.sql.size() + " selects)");
assertThat(Hibernate.isInitialized(ref)).isFalse();
assertThat(db.sql).isEmpty();
}
try (Session s = db.sessionFactory.openSession()) {
Author detached = new Author("[email protected]", "Ann Smith");
detached.id = id;
s.beginTransaction();
Author managed = s.merge(detached);
t.line("merge(detached) -> returns a different instance: " + (managed != detached) + ", name now " + managed.name);
s.getTransaction().commit();
assertThat(managed).isNotSameAs(detached);
assertThat(managed.name).isEqualTo("Ann Smith");
}
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Author extra = new Author("[email protected]", "Bob");
s.persist(extra);
s.getTransaction().commit();
Long bobId = extra.id;
List<Author> many = s.findMultiple(Author.class, List.of(id, bobId, 999L));
t.line("findMultiple(ids 3) -> " + many.size() + " entries, missing id gives: " + many.get(2));
assertThat(many).hasSize(3);
assertThat(many.get(2)).isNull();
}
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Author a = s.find(Author.class, id);
s.remove(a);
s.getTransaction().commit();
t.line("remove(a) -> find afterwards: " + s.find(Author.class, id));
assertThat(s.find(Author.class, id)).isNull();
}
}
}
@Test
void hql() {
try (Db db = new Db(Author.class, Book.class); Transcript t = new Transcript("11-cheat-hql")) {
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Author ann = new Author("[email protected]", "Ann");
Author bob = new Author("[email protected]", "Bob");
s.persist(ann);
s.persist(bob);
s.persist(new Book("Alpha", 100, ann));
s.persist(new Book("Beta", 300, ann));
s.persist(new Book("Gamma", null, bob));
s.getTransaction().commit();
}
try (Session s = db.sessionFactory.openSession()) {
List<String> named = s.createSelectionQuery("select b.title from Book b where b.pages > :min order by b.title", String.class)
.setParameter("min", 150).getResultList();
t.line("named parameter : " + named);
assertThat(named).containsExactly("Beta");
List<String> ordinal = s.createSelectionQuery("select b.title from Book b where b.pages > ?1 order by b.title", String.class)
.setParameter(1, 50).getResultList();
t.line("?1 parameter : " + ordinal);
assertThat(ordinal).containsExactly("Alpha", "Beta");
List<String> page = s.createSelectionQuery("select b.title from Book b order by b.title limit 2 offset 1", String.class).getResultList();
t.line("limit 2 offset 1 in HQL : " + page);
assertThat(page).containsExactly("Beta", "Gamma");
List<String> nulls = s.createSelectionQuery("select b.title from Book b order by b.pages desc nulls last", String.class).getResultList();
t.line("order by ... nulls last : " + nulls);
assertThat(nulls).containsExactly("Beta", "Alpha", "Gamma");
Book none = s.createSelectionQuery("from Book b where b.title = 'Nope'", Book.class).getSingleResultOrNull();
t.line("getSingleResultOrNull() : " + none);
assertThat(none).isNull();
List<Book> fetched = s.createSelectionQuery("select b from Book b join fetch b.author where b.title = 'Alpha'", Book.class).getResultList();
t.line("join fetch : author initialized = " + Hibernate.isInitialized(fetched.get(0).author));
assertThat(Hibernate.isInitialized(fetched.get(0).author)).isTrue();
Long count = s.createSelectionQuery("select count(b) from Book b where b.author.name = :n", Long.class).setParameter("n", "Ann").getSingleResult();
t.line("count over an association : " + count);
assertThat(count).isEqualTo(2L);
}
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
int updated = s.createMutationQuery("update Book b set b.pages = b.pages + 1 where b.pages is not null").executeUpdate();
s.getTransaction().commit();
t.line("createMutationQuery update: " + updated + " rows");
assertThat(updated).isEqualTo(2);
}
}
}
@Test
void statelessSession() {
try (Db db = new Db(Author.class); Transcript t = new Transcript("12-cheat-stateless-session")) {
try (StatelessSession s = db.sessionFactory.openStatelessSession()) {
s.beginTransaction();
for (int i = 1; i <= 3; i++) {
s.insert(new Author("user" + i + "@example.com", "User " + i));
}
s.getTransaction().commit();
t.line("insert x3 (no persistence context) done");
}
try (StatelessSession s = db.sessionFactory.openStatelessSession()) {
Author a1 = s.get(Author.class, 1L);
Author a1again = s.get(Author.class, 1L);
t.line("get twice -> same instance: " + (a1 == a1again) + " (a stateless session has no first-level cache)");
assertThat(a1).isNotNull();
assertThat(a1).isNotSameAs(a1again);
t.line("rows in table: " + db.jdbc("select count(*) from Author"));
assertThat(db.jdbc("select count(*) from Author")).containsExactly("3");
}
}
}
@Test
void naturalId() {
try (Db db = new Db(Author.class); Transcript t = new Transcript("13-cheat-natural-id")) {
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
s.persist(new Author("[email protected]", "Ann"));
s.getTransaction().commit();
}
try (Session s = db.sessionFactory.openSession()) {
db.clear();
Author a = s.bySimpleNaturalId(Author.class).load("[email protected]");
t.line("bySimpleNaturalId(...).load -> " + a.name);
t.line("sql: " + db.sql.get(0));
assertThat(a.name).isEqualTo("Ann");
assertThat(db.sql.get(0)).contains("email");
}
}
}
@Test
void softDelete() {
try (Db db = new Db(Note.class); Transcript t = new Transcript("14-cheat-soft-delete")) {
Long id;
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Note n = new Note("remember");
s.persist(n);
id = n.id;
s.getTransaction().commit();
}
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
db.clear();
s.remove(s.find(Note.class, id));
s.getTransaction().commit();
t.line("remove(note) ran: " + db.sql.get(db.sql.size() - 1));
assertThat(db.sql.get(db.sql.size() - 1)).startsWith("update");
}
try (Session s = db.sessionFactory.openSession()) {
t.line("find afterwards : " + s.find(Note.class, id));
t.line("row still in the table: " + db.jdbc("select * from Note"));
assertThat(s.find(Note.class, id)).isNull();
assertThat(db.jdbc("select count(*) from Note")).containsExactly("1");
}
}
}
@Test
void recordEmbeddable() {
try (Db db = new Db(Shop.class); Transcript t = new Transcript("15-cheat-record-embeddable")) {
Long id;
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Shop shop = new Shop("Corner", new Address("1 Main St", "Pune"));
s.persist(shop);
id = shop.id;
s.getTransaction().commit();
}
try (Session s = db.sessionFactory.openSession()) {
Shop shop = s.find(Shop.class, id);
t.line("loaded: " + shop.address);
t.line("columns: " + db.jdbc("select column_name from information_schema.columns where table_name = 'SHOP' order by column_name"));
assertThat(shop.address).isEqualTo(new Address("1 Main St", "Pune"));
}
}
}
}
@@ -0,0 +1,76 @@
package com.ankurm.migration;
import org.hibernate.SessionFactory;
import org.hibernate.resource.jdbc.spi.StatementInspector;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/** Plain-Hibernate bootstrap against in-memory H2. */
final class Db implements AutoCloseable {
private static final AtomicInteger SEQ = new AtomicInteger();
final List<String> sql = java.util.Collections.synchronizedList(new ArrayList<>());
final SessionFactory sessionFactory;
private final String url;
Db(Class<?>... entities) {
this.url = "jdbc:h2:mem:mig" + SEQ.incrementAndGet() + ";DB_CLOSE_DELAY=-1";
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("jakarta.persistence.jdbc.url", url)
.applySetting("jakarta.persistence.jdbc.user", "sa")
.applySetting("jakarta.persistence.jdbc.password", "")
.applySetting("hibernate.hbm2ddl.auto", "create-drop")
.applySetting("hibernate.session_factory.statement_inspector", (StatementInspector) x -> {
sql.add(x);
return x;
})
.build();
MetadataSources sources = new MetadataSources(registry);
for (Class<?> e : entities) {
sources.addAnnotatedClass(e);
}
this.sessionFactory = sources.buildMetadata().buildSessionFactory();
}
void clear() {
sql.clear();
}
/** Raw JDBC against the same in-memory database; columns joined with a space. */
List<String> jdbc(String query) {
List<String> rows = new ArrayList<>();
try (Connection c = DriverManager.getConnection(url, "sa", "");
Statement st = c.createStatement();
ResultSet rs = st.executeQuery(query)) {
int n = rs.getMetaData().getColumnCount();
while (rs.next()) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= n; i++) {
if (i > 1) {
sb.append(' ');
}
sb.append(rs.getString(i));
}
rows.add(sb.toString());
}
} catch (SQLException e) {
throw new IllegalStateException(e);
}
return rows;
}
@Override
public void close() {
sessionFactory.close();
}
}
@@ -0,0 +1,64 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.hibernate.Session;
import org.junit.jupiter.api.Test;
/** The same probes as the Hibernate 5 module, run on 7.4.14.Final. Output goes to ../output. */
class MigrationProbeTest {
private static final String V = "7.4.14";
@Test
void schemaDefaults() {
try (Db db = new Db(Probe.class); Transcript t = new Transcript("06-schema-" + V)) {
t.line("Hibernate " + V + ", H2, @GeneratedValue with no strategy");
t.line("-- sequences (name, increment)");
List<String> seq = db.jdbc("select sequence_name, increment from information_schema.sequences order by 1");
seq.forEach(t::line);
t.line("-- columns of PROBE (name, type)");
List<String> cols = db.jdbc("select column_name, data_type from information_schema.columns where table_name = 'PROBE' order by column_name");
cols.forEach(t::line);
assertThat(seq).isNotEmpty();
assertThat(cols).hasSize(11);
}
}
@Test
void legacyQueries() {
try (Db db = new Db(Probe.class); Transcript t = new Transcript("07-legacy-queries-" + V)) {
t.line("Hibernate " + V);
try (Session s = db.sessionFactory.openSession()) {
s.beginTransaction();
Probe p = new Probe("alpha");
s.persist(p);
t.line("session.persist(...) assigned the id: " + p.id + " (" + p.id.getClass().getSimpleName() + ")");
s.getTransaction().commit();
assertThat(p.id).isNotNull();
}
try (Session s = db.sessionFactory.openSession()) {
String ordinal = outcome(() -> s.createQuery("from Probe p where p.name = ?1").setParameter(1, "alpha").list());
t.line("HQL with ?1 : " + ordinal);
String bare = outcome(() -> s.createQuery("from Probe p where p.name = ?").setParameter(0, "alpha").list());
t.line("HQL with a bare ? : " + bare);
String enumInt = outcome(() -> s.createQuery("from Probe p where p.byOrdinal = 0").list());
t.line("HQL enum compared with 0 : " + enumInt);
assertThat(ordinal).startsWith("1 row");
assertThat(bare).startsWith("failed");
assertThat(enumInt).startsWith("0 row");
}
}
}
static String outcome(java.util.function.Supplier<List<?>> query) {
try {
return query.get().size() + " row(s)";
} catch (RuntimeException e) {
String msg = String.valueOf(e.getMessage()).lines().findFirst().orElse("");
return "failed: " + e.getClass().getSimpleName() + ": " + (msg.length() > 140 ? msg.substring(0, 140) + "..." : msg);
}
}
}
@@ -0,0 +1,23 @@
package com.ankurm.migration;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hibernate.annotations.SoftDelete;
@Entity
@SoftDelete
public class Note {
@Id
@GeneratedValue
Long id;
String text;
public Note() {
}
Note(String text) {
this.text = text;
}
}
@@ -0,0 +1,46 @@
package com.ankurm.migration;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.util.UUID;
/** One field of each type whose default column mapping is worth comparing, and a generated id. */
@Entity
public class Probe {
@Id
@GeneratedValue
Long id;
String name;
boolean active;
Instant createdAt;
LocalDateTime occurredAt;
UUID uuid;
Duration span;
BigDecimal price;
byte[] payload;
@Enumerated(EnumType.ORDINAL)
Colour byOrdinal;
@Enumerated(EnumType.STRING)
Colour byName;
public enum Colour { RED, GREEN }
public Probe() {
}
Probe(String name) {
this.name = name;
}
}
@@ -0,0 +1,26 @@
package com.ankurm.migration;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
@Entity
public class Shop {
@Id
@GeneratedValue
Long id;
String name;
@Embedded
Address address;
public Shop() {
}
Shop(String name, Address address) {
this.name = name;
this.address = address;
}
}
@@ -0,0 +1,33 @@
package com.ankurm.migration;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/** Collects lines and writes them to output/NN-name.txt when closed. The tests assert what they print. */
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");
}
Transcript line(String s) {
lines.add(s);
return this;
}
@Override
public void close() {
try {
Files.createDirectories(file.getParent());
Files.write(file, lines);
} catch (IOException e) {
throw new IllegalStateException(e);
}
}
}
@@ -0,0 +1,56 @@
package com.ankurm.migration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.nio.file.Files;
import java.nio.file.Path;
import org.hibernate.SessionFactory;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.junit.jupiter.api.Test;
/** Opens the database the Hibernate 5.6.15 module created, with the same entity, and asks Hibernate 7 to validate it. */
class ValidateLegacySchemaTest {
@Test
void validateAndRunAgainstSchemaCreatedByHibernate5() throws Exception {
Path dir = Path.of("..", "target", "legacy-db");
assumeTrue(Files.exists(dir.resolve("probe.mv.db")), "run the h5 module first (scripts/run-all.sh does)");
String url = "jdbc:h2:file:" + dir.toAbsolutePath().normalize() + "/probe";
try (Transcript t = new Transcript("08-validate-against-5.6.15-schema-7.4.14")) {
t.line("Hibernate 7.4.14 opening the H2 file database written by Hibernate 5.6.15 (same Probe entity)");
String validate = open(url, "validate");
t.line("hbm2ddl.auto=validate: " + validate);
assertThat(validate).startsWith("failed");
String none = open(url, "none");
t.line("hbm2ddl.auto=none : " + none);
}
}
private static String open(String url, String mode) {
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
.applySetting("jakarta.persistence.jdbc.url", url)
.applySetting("jakarta.persistence.jdbc.user", "sa")
.applySetting("jakarta.persistence.jdbc.password", "")
.applySetting("hibernate.hbm2ddl.auto", mode)
.build();
try (SessionFactory sf = new MetadataSources(registry).addAnnotatedClass(Probe.class).buildMetadata().buildSessionFactory()) {
try (var s = sf.openSession()) {
Probe p = s.createSelectionQuery("from Probe", Probe.class).getResultList().get(0);
return "opened; loaded the row written by 5.6.15: name=" + p.name + ", uuid=" + p.uuid;
} catch (RuntimeException e) {
return "opened, but the first query failed: " + e.getClass().getSimpleName() + ": " + first(e);
}
} catch (RuntimeException e) {
return "failed: " + e.getClass().getSimpleName() + ": " + first(e);
}
}
private static String first(Throwable e) {
String m = String.valueOf(e.getMessage()).lines().findFirst().orElse("");
return m.length() > 220 ? m.substring(0, 220) + "..." : m;
}
}