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:
@@ -344,3 +344,4 @@ Modules with their own `pom.xml` that run plain Hibernate without the Spring Boo
|
|||||||
| [`h8-preview`](h8-preview/) | Hibernate ORM 8 beta against Hibernate 7: what breaks |
|
| [`h8-preview`](h8-preview/) | Hibernate ORM 8 beta against Hibernate 7: what breaks |
|
||||||
| [`inheritance`](inheritance/) | `SINGLE_TABLE` vs `JOINED` vs `TABLE_PER_CLASS` vs `@MappedSuperclass`: generated DDL, SQL, failures and timings (Hibernate `7.4.13.Final`) |
|
| [`inheritance`](inheritance/) | `SINGLE_TABLE` vs `JOINED` vs `TABLE_PER_CLASS` vs `@MappedSuperclass`: generated DDL, SQL, failures and timings (Hibernate `7.4.13.Final`) |
|
||||||
| [`locking`](locking/) | Optimistic (`@Version`, retries) vs pessimistic (`PESSIMISTIC_WRITE`, `NOWAIT`, `SKIP LOCKED`) locking, lost updates and deadlocks on H2 (Hibernate `7.4.14.Final`) |
|
| [`locking`](locking/) | Optimistic (`@Version`, retries) vs pessimistic (`PESSIMISTIC_WRITE`, `NOWAIT`, `SKIP LOCKED`) locking, lost updates and deadlocks on H2 (Hibernate `7.4.14.Final`) |
|
||||||
|
| [`migration-5-to-7`](migration-5-to-7/) | Hibernate `5.6.15.Final` vs `7.4.14.Final`: jar diff, Hibernate 5 code compiled against both, schema defaults, `hbm2ddl` validation of a Hibernate 5 schema, and cheat-sheet snippets |
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# migration-5-to-7
|
||||||
|
|
||||||
|
Hibernate ORM **5.6.15.Final** (`org.hibernate:hibernate-core`, `javax.persistence`) against **7.4.14.Final**
|
||||||
|
(`org.hibernate.orm:hibernate-core`, `jakarta.persistence`), on in-memory and file H2 2.4.240. Companion to the
|
||||||
|
Hibernate 5 to 7 migration guide and cheat sheet on [ankurm.com](https://ankurm.com).
|
||||||
|
|
||||||
|
| path | what it is |
|
||||||
|
|---|---|
|
||||||
|
| `h5/` | the "before" side: the same probes on Hibernate 5.6.15 |
|
||||||
|
| `h7/` | the "after" side: the probes plus the cheat-sheet snippets (`CheatSheetTest`) on Hibernate 7.4.14 |
|
||||||
|
| `src/compat/` | Hibernate 5 style and Hibernate 7 style sources that are only compiled, never run |
|
||||||
|
| `scripts/api_diff.py` | jar diff of the two `hibernate-core` jars (`javax.persistence` read as `jakarta.persistence`) |
|
||||||
|
| `scripts/compile-old-apis.sh` | compiles `src/compat` against both versions |
|
||||||
|
| `scripts/schema_diff.py` | lines up the two `06-schema-*` files |
|
||||||
|
| `scripts/run-all.sh` | everything above; rewrites `output/` |
|
||||||
|
|
||||||
|
Needs JDK 17 or newer (built with 25), Maven 3.9, Python 3 and access to Maven Central. Hibernate 5.6.15 pulls Byte Buddy
|
||||||
|
1.12.18, which does not know a current JDK, so `h5/pom.xml` pins a newer Byte Buddy.
|
||||||
|
|
||||||
|
`output/02-removed-api.txt` lists exact signatures that disappeared (generics and `throws` ignored, internal and `spi`
|
||||||
|
packages left out). Parameter type changes such as `Serializable` to `Object` count as removed there, so treat it as a list to
|
||||||
|
check, not a list of deletions; the compile checks in `output/03` to `output/09` are the arbiter.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>migration-h5</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>migration-h5</name>
|
||||||
|
<description>The "before" side: Hibernate ORM 5.6.15.Final with javax.persistence, run against in-memory H2</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<hibernate.version>5.6.15.Final</hibernate.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<!-- Hibernate 5.6.15 pulls Byte Buddy 1.12.18, which does not know the JDK this runs on; a current one does -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.bytebuddy</groupId>
|
||||||
|
<artifactId>byte-buddy</artifactId>
|
||||||
|
<version>1.17.7</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<!-- note the group id: org.hibernate here, org.hibernate.orm from 6.0 on -->
|
||||||
|
<groupId>org.hibernate</groupId>
|
||||||
|
<artifactId>hibernate-core</artifactId>
|
||||||
|
<version>${hibernate.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<version>2.4.240</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>6.1.3</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.27.7</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.ankurm.migration;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hibernate.Session;
|
||||||
|
import org.hibernate.criterion.Restrictions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
/** The same probes as the Hibernate 7 module, run on 5.6.15.Final. Output goes to ../output. */
|
||||||
|
class BaselineTest {
|
||||||
|
|
||||||
|
private static final String V = "5.6.15";
|
||||||
|
|
||||||
|
@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();
|
||||||
|
Serializable id = s.save(new Probe("alpha"));
|
||||||
|
t.line("session.save(...) returned: " + id + " (" + id.getClass().getSimpleName() + ")");
|
||||||
|
s.getTransaction().commit();
|
||||||
|
assertThat(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);
|
||||||
|
String criteria = outcome(() -> s.createCriteria(Probe.class).add(Restrictions.eq("name", "alpha")).list());
|
||||||
|
t.line("session.createCriteria(...) : " + criteria);
|
||||||
|
assertThat(ordinal).startsWith("1 row");
|
||||||
|
assertThat(bare).startsWith("failed");
|
||||||
|
assertThat(enumInt).startsWith("0 row");
|
||||||
|
assertThat(criteria).startsWith("1 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,66 @@
|
|||||||
|
package com.ankurm.migration;
|
||||||
|
|
||||||
|
import org.hibernate.SessionFactory;
|
||||||
|
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 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("hibernate.connection.url", url)
|
||||||
|
.applySetting("hibernate.connection.username", "sa")
|
||||||
|
.applySetting("hibernate.connection.password", "")
|
||||||
|
.applySetting("hibernate.hbm2ddl.auto", "create-drop")
|
||||||
|
.build();
|
||||||
|
MetadataSources sources = new MetadataSources(registry);
|
||||||
|
for (Class<?> e : entities) {
|
||||||
|
sources.addAnnotatedClass(e);
|
||||||
|
}
|
||||||
|
this.sessionFactory = sources.buildMetadata().buildSessionFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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,39 @@
|
|||||||
|
package com.ankurm.migration;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.hibernate.Session;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/** Creates the schema Hibernate 5.6.15 generates, in an H2 file database that the Hibernate 7 module opens later. */
|
||||||
|
class LegacySchemaTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createSchemaFile() throws Exception {
|
||||||
|
Path dir = Path.of("..", "target", "legacy-db");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
for (var f : dir.toFile().listFiles()) {
|
||||||
|
f.delete();
|
||||||
|
}
|
||||||
|
StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
|
||||||
|
.applySetting("hibernate.connection.url", "jdbc:h2:file:" + dir.toAbsolutePath().normalize() + "/probe")
|
||||||
|
.applySetting("hibernate.connection.username", "sa")
|
||||||
|
.applySetting("hibernate.connection.password", "")
|
||||||
|
.applySetting("hibernate.hbm2ddl.auto", "create")
|
||||||
|
.build();
|
||||||
|
try (SessionFactory sf = new MetadataSources(registry).addAnnotatedClass(Probe.class).buildMetadata().buildSessionFactory();
|
||||||
|
Session s = sf.openSession()) {
|
||||||
|
s.beginTransaction();
|
||||||
|
s.persist(new Probe("legacy row"));
|
||||||
|
s.getTransaction().commit();
|
||||||
|
}
|
||||||
|
assertThat(Files.exists(dir.resolve("probe.mv.db"))).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.ankurm.migration;
|
||||||
|
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EnumType;
|
||||||
|
import javax.persistence.Enumerated;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.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,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,57 @@
|
|||||||
|
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>com.ankurm</groupId>
|
||||||
|
<artifactId>migration-h7</artifactId>
|
||||||
|
<version>1.0.0</version>
|
||||||
|
<name>migration-h7</name>
|
||||||
|
<description>The "after" side: Hibernate ORM 7.4.14.Final with jakarta.persistence, run against in-memory H2</description>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.release>17</maven.compiler.release>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<hibernate.version>7.4.14.Final</hibernate.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.hibernate.orm</groupId>
|
||||||
|
<artifactId>hibernate-core</artifactId>
|
||||||
|
<version>${hibernate.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>jakarta.persistence</groupId>
|
||||||
|
<artifactId>jakarta.persistence-api</artifactId>
|
||||||
|
<version>3.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<version>2.4.240</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>6.1.3</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.27.7</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
5.x 7.x
|
||||||
|
coordinates org.hibernate:hibernate-core:5.6.15.Final org.hibernate.orm:hibernate-core:7.4.14.Final
|
||||||
|
class files Java 8 Java 17
|
||||||
|
persistence javax.persistence (JPA 2.2) jakarta.persistence (Jakarta Persistence 3.2)
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
== hibernate-core: public classes missing: 828, members removed from classes that remain: 2735, new classes: 5879
|
||||||
|
of the missing classes: 59 were deprecated in 5.6.15, 137 have a class of the same simple name somewhere in 7.4.14
|
||||||
|
packages with the most missing classes:
|
||||||
|
75 org.hibernate.dialect
|
||||||
|
74 org.hibernate.type
|
||||||
|
62 org.hibernate.type.descriptor.java
|
||||||
|
59 org.hibernate.criterion
|
||||||
|
53 org.hibernate.cfg
|
||||||
|
36 org.hibernate.type.descriptor.sql
|
||||||
|
35 org.hibernate.annotations
|
||||||
|
27 org.hibernate.dialect.function
|
||||||
|
25 org.hibernate.sql.ordering.antlr
|
||||||
|
23 org.hibernate.sql
|
||||||
|
23 org.hibernate.tool.hbm2ddl
|
||||||
|
22 org.hibernate.cfg.annotations
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
|||||||
|
Hibernate 5.6.15, compiling OldHibernate.java
|
||||||
|
javac exit status: 0
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
Hibernate 7.4.14, compiling OldHibernate.java
|
||||||
|
src/compat/OldHibernate.java:4: error: cannot find symbol
|
||||||
|
import org.hibernate.Criteria;
|
||||||
|
^
|
||||||
|
symbol: class Criteria
|
||||||
|
location: package org.hibernate
|
||||||
|
src/compat/OldHibernate.java:8: error: package org.hibernate.criterion does not exist
|
||||||
|
import org.hibernate.criterion.Restrictions;
|
||||||
|
^
|
||||||
|
src/compat/OldHibernate.java:15: error: cannot find symbol
|
||||||
|
return s.save(o);
|
||||||
|
^
|
||||||
|
symbol: method save(Object)
|
||||||
|
location: variable s of type Session
|
||||||
|
src/compat/OldHibernate.java:19: error: cannot find symbol
|
||||||
|
s.update(o);
|
||||||
|
^
|
||||||
|
symbol: method update(Object)
|
||||||
|
location: variable s of type Session
|
||||||
|
src/compat/OldHibernate.java:23: error: cannot find symbol
|
||||||
|
s.saveOrUpdate(o);
|
||||||
|
^
|
||||||
|
symbol: method saveOrUpdate(Object)
|
||||||
|
location: variable s of type Session
|
||||||
|
src/compat/OldHibernate.java:27: error: cannot find symbol
|
||||||
|
s.delete(o);
|
||||||
|
^
|
||||||
|
symbol: method delete(Object)
|
||||||
|
location: variable s of type Session
|
||||||
|
src/compat/OldHibernate.java:31: error: incompatible types: void cannot be converted to Object
|
||||||
|
return s.load(Object.class, 1L);
|
||||||
|
^
|
||||||
|
src/compat/OldHibernate.java:35: error: cannot find symbol
|
||||||
|
Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
|
||||||
|
^
|
||||||
|
symbol: class Criteria
|
||||||
|
location: class OldHibernate
|
||||||
|
src/compat/OldHibernate.java:35: error: cannot find symbol
|
||||||
|
Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
|
||||||
|
^
|
||||||
|
symbol: method createCriteria(Class<Object>)
|
||||||
|
location: variable s of type Session
|
||||||
|
src/compat/OldHibernate.java:35: error: cannot find symbol
|
||||||
|
Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
|
||||||
|
^
|
||||||
|
symbol: variable Restrictions
|
||||||
|
location: class OldHibernate
|
||||||
|
src/compat/OldHibernate.java:40: error: cannot find symbol
|
||||||
|
return s.createSQLQuery("select 1").list();
|
||||||
|
^
|
||||||
|
symbol: method createSQLQuery(String)
|
||||||
|
location: variable s of type Session
|
||||||
|
src/compat/OldHibernate.java:44: error: incompatible types: FlushMode cannot be converted to FlushModeType
|
||||||
|
s.setFlushMode(FlushMode.AUTO);
|
||||||
|
^
|
||||||
|
src/compat/OldHibernate.java:48: error: cannot find symbol
|
||||||
|
return sf.getClassMetadata(Object.class);
|
||||||
|
^
|
||||||
|
symbol: method getClassMetadata(Class<Object>)
|
||||||
|
location: variable sf of type SessionFactory
|
||||||
|
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
|
||||||
|
13 errors
|
||||||
|
javac exit status: 1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Hibernate 5.6.15, compiling NewHibernate.java
|
||||||
|
javac exit status: 0
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Hibernate 7.4.14, compiling NewHibernate.java
|
||||||
|
src/compat/NewHibernate.java:34: warning: [deprecation] createNativeQuery(String) in Session has been deprecated
|
||||||
|
return s.createNativeQuery("select 1").getResultList();
|
||||||
|
^
|
||||||
|
1 warning
|
||||||
|
javac exit status: 0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Hibernate 5.6.15, compiling OldAnnotations.java
|
||||||
|
javac exit status: 0
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
Hibernate 7.4.14, compiling OldAnnotations.java
|
||||||
|
src/compat/OldAnnotations.java:3: error: cannot find symbol
|
||||||
|
import org.hibernate.annotations.Where;
|
||||||
|
^
|
||||||
|
symbol: class Where
|
||||||
|
location: package org.hibernate.annotations
|
||||||
|
src/compat/OldAnnotations.java:7: error: cannot find symbol
|
||||||
|
@Where(clause = "deleted = false")
|
||||||
|
^
|
||||||
|
symbol: class Where
|
||||||
|
src/compat/OldAnnotations.java:10: error: cannot find symbol
|
||||||
|
@Type(type = "yes_no")
|
||||||
|
^
|
||||||
|
symbol: method type()
|
||||||
|
location: @interface Type
|
||||||
|
src/compat/OldAnnotations.java:10: error: annotation @Type is missing a default value for the element 'value'
|
||||||
|
@Type(type = "yes_no")
|
||||||
|
^
|
||||||
|
src/compat/OldAnnotations.java:13: warning: [removal] GenericGenerator in org.hibernate.annotations has been deprecated and marked for removal
|
||||||
|
@GenericGenerator(name = "g", strategy = "increment")
|
||||||
|
^
|
||||||
|
src/compat/OldAnnotations.java:13: warning: [removal] strategy() in GenericGenerator has been deprecated and marked for removal
|
||||||
|
@GenericGenerator(name = "g", strategy = "increment")
|
||||||
|
^
|
||||||
|
4 errors
|
||||||
|
2 warnings
|
||||||
|
javac exit status: 1
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
Hibernate 5.6.15, compiling NewAnnotations.java
|
||||||
|
src/compat/NewAnnotations.java:1: error: cannot find symbol
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
^
|
||||||
|
symbol: class JdbcTypeCode
|
||||||
|
location: package org.hibernate.annotations
|
||||||
|
src/compat/NewAnnotations.java:2: error: cannot find symbol
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
^
|
||||||
|
symbol: class SQLRestriction
|
||||||
|
location: package org.hibernate.annotations
|
||||||
|
src/compat/NewAnnotations.java:3: error: cannot find symbol
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
^
|
||||||
|
symbol: class SqlTypes
|
||||||
|
location: package org.hibernate.type
|
||||||
|
src/compat/NewAnnotations.java:6: error: cannot find symbol
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
|
^
|
||||||
|
symbol: class SQLRestriction
|
||||||
|
src/compat/NewAnnotations.java:9: error: cannot find symbol
|
||||||
|
@JdbcTypeCode(SqlTypes.CHAR)
|
||||||
|
^
|
||||||
|
symbol: class JdbcTypeCode
|
||||||
|
location: class NewAnnotations
|
||||||
|
src/compat/NewAnnotations.java:9: error: cannot find symbol
|
||||||
|
@JdbcTypeCode(SqlTypes.CHAR)
|
||||||
|
^
|
||||||
|
symbol: variable SqlTypes
|
||||||
|
location: class NewAnnotations
|
||||||
|
6 errors
|
||||||
|
javac exit status: 1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Hibernate 7.4.14, compiling NewAnnotations.java
|
||||||
|
javac exit status: 0
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Hibernate 5.6.15, H2, @GeneratedValue with no strategy
|
||||||
|
-- sequences (name, increment)
|
||||||
|
HIBERNATE_SEQUENCE 1
|
||||||
|
-- columns of PROBE (name, type)
|
||||||
|
ACTIVE BOOLEAN
|
||||||
|
BYNAME CHARACTER VARYING
|
||||||
|
BYORDINAL INTEGER
|
||||||
|
CREATEDAT TIMESTAMP
|
||||||
|
ID BIGINT
|
||||||
|
NAME CHARACTER VARYING
|
||||||
|
OCCURREDAT TIMESTAMP
|
||||||
|
PAYLOAD BINARY VARYING
|
||||||
|
PRICE NUMERIC
|
||||||
|
SPAN BIGINT
|
||||||
|
UUID BINARY
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Hibernate 7.4.14, H2, @GeneratedValue with no strategy
|
||||||
|
-- sequences (name, increment)
|
||||||
|
PROBE_SEQ 50
|
||||||
|
-- columns of PROBE (name, type)
|
||||||
|
ACTIVE BOOLEAN
|
||||||
|
BYNAME ENUM
|
||||||
|
BYORDINAL TINYINT
|
||||||
|
CREATEDAT TIMESTAMP WITH TIME ZONE
|
||||||
|
ID BIGINT
|
||||||
|
NAME CHARACTER VARYING
|
||||||
|
OCCURREDAT TIMESTAMP
|
||||||
|
PAYLOAD BINARY VARYING
|
||||||
|
PRICE NUMERIC
|
||||||
|
SPAN NUMERIC
|
||||||
|
UUID UUID
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
Hibernate 5.6.15 Hibernate 7.4.14
|
||||||
|
-- id sequences (name, increment)
|
||||||
|
HIBERNATE_SEQUENCE 1 PROBE_SEQ 50
|
||||||
|
-- column types in H2
|
||||||
|
ACTIVE BOOLEAN BOOLEAN (same)
|
||||||
|
BYNAME CHARACTER VARYING ENUM <-- differs
|
||||||
|
BYORDINAL INTEGER TINYINT <-- differs
|
||||||
|
CREATEDAT TIMESTAMP TIMESTAMP WITH TIME ZONE <-- differs
|
||||||
|
ID BIGINT BIGINT (same)
|
||||||
|
NAME CHARACTER VARYING CHARACTER VARYING (same)
|
||||||
|
OCCURREDAT TIMESTAMP TIMESTAMP (same)
|
||||||
|
PAYLOAD BINARY VARYING BINARY VARYING (same)
|
||||||
|
PRICE NUMERIC NUMERIC (same)
|
||||||
|
SPAN BIGINT NUMERIC <-- differs
|
||||||
|
UUID BINARY UUID <-- differs
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Hibernate 5.6.15
|
||||||
|
session.save(...) returned: 1 (Long)
|
||||||
|
HQL with ?1 : 1 row(s)
|
||||||
|
HQL with a bare ? : failed: IllegalArgumentException: org.hibernate.QueryException: Legacy-style query parameters (`?`) are no longer supported; use JPA-style ordinal parameters (e.g., `?1`) ins...
|
||||||
|
HQL enum compared with 0 : 0 row(s)
|
||||||
|
session.createCriteria(...) : 1 row(s)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
Hibernate 7.4.14
|
||||||
|
session.persist(...) assigned the id: 1 (Long)
|
||||||
|
HQL with ?1 : 1 row(s)
|
||||||
|
HQL with a bare ? : failed: IllegalArgumentException: org.hibernate.query.ParameterLabelException: Unlabeled ordinal parameter ('?' rather than ?1)
|
||||||
|
HQL enum compared with 0 : 0 row(s)
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
Hibernate 7.4.14 opening the H2 file database written by Hibernate 5.6.15 (same Probe entity)
|
||||||
|
hbm2ddl.auto=validate: failed: SchemaManagementException: Schema validation: wrong column type encountered in column [span] in table [Probe]; found [bigint (Types#BIGINT)], but expecting [numeric(21,0) (Types#NUMERIC)]
|
||||||
|
hbm2ddl.auto=none : opened; loaded the row written by 5.6.15: name=legacy row, uuid=null
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Hibernate 5.6.15, compiling JakartaNamespace.java
|
||||||
|
src/compat/JakartaNamespace.java:1: error: package jakarta.persistence does not exist
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
^
|
||||||
|
src/compat/JakartaNamespace.java:3: error: cannot find symbol
|
||||||
|
@Entity
|
||||||
|
^
|
||||||
|
symbol: class Entity
|
||||||
|
2 errors
|
||||||
|
javac exit status: 1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Hibernate 7.4.14, compiling JakartaNamespace.java
|
||||||
|
javac exit status: 0
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Hibernate 5.6.15, compiling JavaxNamespace.java
|
||||||
|
javac exit status: 0
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Hibernate 7.4.14, compiling JavaxNamespace.java
|
||||||
|
src/compat/JavaxNamespace.java:1: error: package javax.persistence does not exist
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
^
|
||||||
|
src/compat/JavaxNamespace.java:3: error: cannot find symbol
|
||||||
|
@Entity
|
||||||
|
^
|
||||||
|
symbol: class Entity
|
||||||
|
2 errors
|
||||||
|
javac exit status: 1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
persist(a) -> id assigned on the object: 1
|
||||||
|
find(Author, id) -> Ann (1 select)
|
||||||
|
getReference(Author, id) -> initialized: false (0 selects)
|
||||||
|
merge(detached) -> returns a different instance: true, name now Ann Smith
|
||||||
|
findMultiple(ids 3) -> 3 entries, missing id gives: null
|
||||||
|
remove(a) -> find afterwards: null
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
named parameter : [Beta]
|
||||||
|
?1 parameter : [Alpha, Beta]
|
||||||
|
limit 2 offset 1 in HQL : [Beta, Gamma]
|
||||||
|
order by ... nulls last : [Beta, Alpha, Gamma]
|
||||||
|
getSingleResultOrNull() : null
|
||||||
|
join fetch : author initialized = true
|
||||||
|
count over an association : 2
|
||||||
|
createMutationQuery update: 2 rows
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
insert x3 (no persistence context) done
|
||||||
|
get twice -> same instance: false (a stateless session has no first-level cache)
|
||||||
|
rows in table: [3]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
bySimpleNaturalId(...).load -> Ann
|
||||||
|
sql: select a1_0.id,a1_0.email,a1_0.name from Author a1_0 where a1_0.email=?
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
remove(note) ran: update Note set deleted=true where id=? and deleted=false
|
||||||
|
find afterwards : null
|
||||||
|
row still in the table: [TRUE 1 remember]
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
loaded: Address[street=1 Main St, city=Pune]
|
||||||
|
columns: [CITY, ID, NAME, STREET]
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Compare the public API of hibernate-core 5.6.15.Final (org.hibernate) with 7.4.14.Final (org.hibernate.orm).
|
||||||
|
|
||||||
|
javax.persistence is read as jakarta.persistence, so the namespace move does not drown the real changes.
|
||||||
|
Writes output/01-versions-and-baseline.txt, output/02-removed-api.txt and output/02-removed-api-summary.txt.
|
||||||
|
Needs a JDK (javap) and access to Maven Central.
|
||||||
|
"""
|
||||||
|
import collections
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
import urllib.error
|
||||||
|
import urllib.request
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||||
|
JARS = ROOT / "target" / "jars"
|
||||||
|
OUT = ROOT / "output"
|
||||||
|
JAVAP = os.path.join(os.environ["JAVA_HOME"], "bin", "javap") if os.environ.get("JAVA_HOME") else "javap"
|
||||||
|
|
||||||
|
OLD = ("org/hibernate", "5.6.15.Final")
|
||||||
|
NEW = ("org/hibernate/orm", "7.4.14.Final")
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(url, dest):
|
||||||
|
if dest.exists():
|
||||||
|
return
|
||||||
|
for _ in range(10):
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url) as r:
|
||||||
|
dest.write_bytes(r.read())
|
||||||
|
return
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code != 429:
|
||||||
|
raise
|
||||||
|
time.sleep(5)
|
||||||
|
raise RuntimeError("rate limited: " + url)
|
||||||
|
|
||||||
|
|
||||||
|
def jar(side):
|
||||||
|
group, v = side
|
||||||
|
dest = JARS / f"hibernate-core-{v}.jar"
|
||||||
|
fetch(f"https://repo1.maven.org/maven2/{group}/hibernate-core/{v}/hibernate-core-{v}.jar", dest)
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def classes(path):
|
||||||
|
with zipfile.ZipFile(path) as z:
|
||||||
|
return sorted(n[:-6].replace("/", ".") for n in z.namelist()
|
||||||
|
if n.endswith(".class") and not n.endswith("module-info.class") and "META-INF" not in n)
|
||||||
|
|
||||||
|
|
||||||
|
def major(path):
|
||||||
|
with zipfile.ZipFile(path) as z:
|
||||||
|
name = next(n for n in z.namelist() if n.endswith(".class") and "META-INF" not in n and not n.endswith("module-info.class"))
|
||||||
|
return int.from_bytes(z.read(name)[6:8], "big")
|
||||||
|
|
||||||
|
|
||||||
|
def jakarta(text):
|
||||||
|
return text.replace("javax.persistence", "jakarta.persistence").replace("javax.transaction", "jakarta.transaction")
|
||||||
|
|
||||||
|
|
||||||
|
def norm(sig):
|
||||||
|
sig = jakarta(sig)
|
||||||
|
sig = re.sub(r"\s+throws\s+.*$", "", sig)
|
||||||
|
prev = None
|
||||||
|
while prev != sig:
|
||||||
|
prev = sig
|
||||||
|
sig = re.sub(r"<[^<>]*>", "", sig)
|
||||||
|
return " ".join(sig.split())
|
||||||
|
|
||||||
|
|
||||||
|
MEMBER = re.compile(r"^ ((?:public|protected)[^;]*);$")
|
||||||
|
|
||||||
|
|
||||||
|
def dump(path):
|
||||||
|
names = [c for c in classes(path) if ".internal." not in c and ".spi." not in c and not c.startswith("org.hibernate.sql.ast")
|
||||||
|
and not c.startswith("org.hibernate.sql.results") and not c.startswith("org.hibernate.query.sqm.tree")
|
||||||
|
and not c.startswith("org.hibernate.grammars")]
|
||||||
|
result = {}
|
||||||
|
for i in range(0, len(names), 150):
|
||||||
|
chunk = names[i:i + 150]
|
||||||
|
p = subprocess.run([JAVAP, "-v", "-protected", "-cp", str(path)] + chunk, capture_output=True, text=True)
|
||||||
|
for block in re.split(r"(?m)^Classfile ", p.stdout)[1:]:
|
||||||
|
lines = block.splitlines()
|
||||||
|
cls = jakarta(re.search(r"!/(.*)\.class", lines[0]).group(1).replace("/", "."))
|
||||||
|
public, seen_flags, in_pool, cur = False, False, False, None
|
||||||
|
members = {}
|
||||||
|
cls_dep = bool(re.search(r"^Deprecated: true", block, re.M))
|
||||||
|
for line in lines[1:]:
|
||||||
|
if not seen_flags and line.startswith(" flags:"):
|
||||||
|
public = "ACC_PUBLIC" in line
|
||||||
|
seen_flags = True
|
||||||
|
if line.startswith("Constant pool:"):
|
||||||
|
in_pool = True
|
||||||
|
continue
|
||||||
|
if in_pool:
|
||||||
|
if line.startswith("{"):
|
||||||
|
in_pool = False
|
||||||
|
continue
|
||||||
|
m = MEMBER.match(line)
|
||||||
|
if m:
|
||||||
|
cur = " ".join(m.group(1).split())
|
||||||
|
members[cur] = False
|
||||||
|
continue
|
||||||
|
if line.strip() == "Deprecated: true" and cur is not None:
|
||||||
|
members[cur] = True
|
||||||
|
if line.startswith("}"):
|
||||||
|
cur = None
|
||||||
|
result[cls] = (public, cls_dep, members)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
JARS.mkdir(parents=True, exist_ok=True)
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
old, new = jar(OLD), jar(NEW)
|
||||||
|
mo, mn = major(old), major(new)
|
||||||
|
versions = [
|
||||||
|
f"{'':<12}{'5.x':<46}{'7.x'}",
|
||||||
|
f"{'coordinates':<12}{'org.hibernate:hibernate-core:5.6.15.Final':<46}org.hibernate.orm:hibernate-core:7.4.14.Final",
|
||||||
|
f"{'class files':<12}{'Java ' + str(mo - 44):<46}Java {mn - 44}",
|
||||||
|
f"{'persistence':<12}{'javax.persistence (JPA 2.2)':<46}jakarta.persistence (Jakarta Persistence 3.2)",
|
||||||
|
]
|
||||||
|
do, dn = dump(old), dump(new)
|
||||||
|
co = {jakarta(c) for c in classes(old)}
|
||||||
|
cn = {jakarta(c) for c in classes(new)}
|
||||||
|
simple_new = collections.defaultdict(list)
|
||||||
|
for c in cn:
|
||||||
|
simple_new[c.split(".")[-1]].append(c)
|
||||||
|
removed_cls, removed_mem = [], []
|
||||||
|
for c in sorted(co - cn):
|
||||||
|
if c in do and do[c][0]:
|
||||||
|
removed_cls.append((c, do[c][1], simple_new.get(c.split(".")[-1], [])))
|
||||||
|
for c in sorted(do):
|
||||||
|
if c in dn and do[c][0]:
|
||||||
|
new_norm = {norm(x) for x in dn[c][2]}
|
||||||
|
for sig, dep in do[c][2].items():
|
||||||
|
if norm(sig) not in new_norm:
|
||||||
|
removed_mem.append((c, jakarta(sig), dep))
|
||||||
|
lines = [f"== hibernate-core: public classes missing: {len(removed_cls)}, members removed from classes that remain: {len(removed_mem)}, "
|
||||||
|
f"new classes: {len(cn - co)}"]
|
||||||
|
for c, dep, same_name in removed_cls:
|
||||||
|
if same_name:
|
||||||
|
note = "same simple name exists in " + ", ".join(sorted(p.rsplit('.', 1)[0] for p in same_name)[:2])
|
||||||
|
else:
|
||||||
|
note = "no class with this name"
|
||||||
|
lines.append(f" class {c} [{'deprecated in 5.6' if dep else 'NOT deprecated in 5.6'}; {note}]")
|
||||||
|
for c, sig, dep in removed_mem:
|
||||||
|
lines.append(f" member {c}: {sig} [{'deprecated in 5.6' if dep else 'NOT deprecated in 5.6'}]")
|
||||||
|
(OUT / "01-versions-and-baseline.txt").write_text("\n".join(versions) + "\n")
|
||||||
|
(OUT / "02-removed-api.txt").write_text("\n".join(lines) + "\n")
|
||||||
|
# summary: counts per package, and how many of the missing classes have a namesake elsewhere
|
||||||
|
per_pkg = collections.Counter(c.rsplit(".", 1)[0] for c, _, _ in removed_cls)
|
||||||
|
dep_n = sum(1 for _, d, _ in removed_cls if d)
|
||||||
|
ren_n = sum(1 for _, _, s in removed_cls if s)
|
||||||
|
summary = [lines[0],
|
||||||
|
f" of the missing classes: {dep_n} were deprecated in 5.6.15, {ren_n} have a class of the same simple name somewhere in 7.4.14",
|
||||||
|
" packages with the most missing classes:"]
|
||||||
|
for pkg, n in per_pkg.most_common(12):
|
||||||
|
summary.append(f" {n:>4} {pkg}")
|
||||||
|
(OUT / "02-removed-api-summary.txt").write_text("\n".join(summary) + "\n")
|
||||||
|
print("\n".join(versions))
|
||||||
|
print("\n".join(summary))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+23
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Compiles src/compat/*.java against Hibernate 5.6.15 (h5/) and 7.4.14 (h7/) and records javac's answer in output/.
|
||||||
|
set -u
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
mkdir -p target output
|
||||||
|
compile() { # version dir file outfile
|
||||||
|
local v=$1 dir=$2 f=$3 out=$4
|
||||||
|
rm -rf target/classes-$f-$v; mkdir -p target/classes-$f-$v
|
||||||
|
{
|
||||||
|
echo "Hibernate $v, compiling $f.java"
|
||||||
|
javac -Xlint:deprecation -proc:none -d target/classes-$f-$v -cp "$(cat target/cp-$v.txt)" src/compat/$f.java 2>&1 | grep -v "^Picked up"
|
||||||
|
echo "javac exit status: ${PIPESTATUS[0]}"
|
||||||
|
} >> $out
|
||||||
|
}
|
||||||
|
for pair in "h5:5.6.15" "h7:7.4.14"; do
|
||||||
|
dir=${pair%%:*}; v=${pair##*:}
|
||||||
|
(cd $dir && mvn -q -B dependency:build-classpath -Dmdep.outputFile=../target/cp-$v.txt >/dev/null) || { echo "classpath failed for $v"; exit 1; }
|
||||||
|
for spec in "OldHibernate:03-old-hibernate-apis" "NewHibernate:04-new-hibernate-apis" "OldAnnotations:05-old-annotations" "NewAnnotations:06-new-annotations" "JavaxNamespace:09-namespace-javax" "JakartaNamespace:09-namespace-jakarta"; do
|
||||||
|
f=${spec%%:*}; n=${spec##*:}
|
||||||
|
out=output/$n-$v.txt; : > $out
|
||||||
|
compile $v $dir $f $out
|
||||||
|
done
|
||||||
|
done
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Rewrites everything in output/. Needs JDK 17+, Maven 3.9, Python 3 and internet access to Maven Central.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
python3 scripts/api_diff.py
|
||||||
|
(cd h5 && mvn -q -B clean test)
|
||||||
|
(cd h7 && mvn -q -B clean test)
|
||||||
|
python3 scripts/schema_diff.py
|
||||||
|
./scripts/compile-old-apis.sh
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Lines up output/06-schema-5.6.15.txt and output/06-schema-7.4.14.txt side by side into output/06-schema-diff.txt."""
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
OUT = pathlib.Path(__file__).resolve().parent.parent / "output"
|
||||||
|
|
||||||
|
|
||||||
|
def read(version):
|
||||||
|
seq, cols, section = {}, {}, None
|
||||||
|
for line in (OUT / f"06-schema-{version}.txt").read_text().splitlines():
|
||||||
|
if line.startswith("-- sequences"):
|
||||||
|
section = "seq"
|
||||||
|
elif line.startswith("-- columns"):
|
||||||
|
section = "col"
|
||||||
|
elif section == "seq":
|
||||||
|
name, inc = line.rsplit(" ", 1)
|
||||||
|
seq[name] = inc
|
||||||
|
elif section == "col":
|
||||||
|
name, typ = line.split(" ", 1)
|
||||||
|
cols[name] = typ
|
||||||
|
return seq, cols
|
||||||
|
|
||||||
|
|
||||||
|
s5, c5 = read("5.6.15")
|
||||||
|
s7, c7 = read("7.4.14")
|
||||||
|
rows = [f"{'':<14}{'Hibernate 5.6.15':<26}{'Hibernate 7.4.14':<26}", "-- id sequences (name, increment)"]
|
||||||
|
rows.append(f"{'':<14}{', '.join(f'{k} {v}' for k, v in s5.items()):<26}{', '.join(f'{k} {v}' for k, v in s7.items())}")
|
||||||
|
rows.append("-- column types in H2")
|
||||||
|
for name in sorted(set(c5) | set(c7)):
|
||||||
|
a, b = c5.get(name, "-"), c7.get(name, "-")
|
||||||
|
rows.append(f"{name:<14}{a:<26}{b:<26}{'(same)' if a == b else '<-- differs'}")
|
||||||
|
(OUT / "06-schema-diff.txt").write_text("\n".join(rows) + "\n")
|
||||||
|
print("\n".join(rows))
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import jakarta.persistence.Entity;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class JakartaNamespace {
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import javax.persistence.Entity;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
class JavaxNamespace {
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.annotations.SQLRestriction;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
|
||||||
|
/** The Hibernate 6 and 7 spelling of the same ideas. This does not compile on 5.6.15. */
|
||||||
|
@SQLRestriction("deleted = false")
|
||||||
|
class NewAnnotations {
|
||||||
|
|
||||||
|
@JdbcTypeCode(SqlTypes.CHAR)
|
||||||
|
boolean flag;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hibernate.FlushMode;
|
||||||
|
import org.hibernate.Session;
|
||||||
|
|
||||||
|
/** The replacements for OldHibernate that exist on both 5.6.15 and 7.4.14, so they can be made before the upgrade. */
|
||||||
|
class NewHibernate {
|
||||||
|
|
||||||
|
void persist(Session s, Object o) {
|
||||||
|
s.persist(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object merge(Session s, Object o) {
|
||||||
|
return s.merge(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(Session s, Object o) {
|
||||||
|
s.remove(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object find(Session s) {
|
||||||
|
return s.find(Object.class, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object reference(Session s) {
|
||||||
|
return s.getReference(Object.class, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Object> query(Session s) {
|
||||||
|
return s.createQuery("from Object o where o.name = :n", Object.class).setParameter("n", "x").getResultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<?> nativeQuery(Session s) {
|
||||||
|
return s.createNativeQuery("select 1").getResultList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void flushMode(Session s) {
|
||||||
|
s.setHibernateFlushMode(FlushMode.AUTO);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import org.hibernate.annotations.GenericGenerator;
|
||||||
|
import org.hibernate.annotations.Type;
|
||||||
|
import org.hibernate.annotations.Where;
|
||||||
|
|
||||||
|
/** Hibernate 5 annotation attributes. The annotations are applied to a plain class, so no persistence import is needed. */
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
@Where(clause = "deleted = false")
|
||||||
|
class OldAnnotations {
|
||||||
|
|
||||||
|
@Type(type = "yes_no")
|
||||||
|
boolean flag;
|
||||||
|
|
||||||
|
@GenericGenerator(name = "g", strategy = "increment")
|
||||||
|
Long id;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hibernate.Criteria;
|
||||||
|
import org.hibernate.FlushMode;
|
||||||
|
import org.hibernate.Session;
|
||||||
|
import org.hibernate.SessionFactory;
|
||||||
|
import org.hibernate.criterion.Restrictions;
|
||||||
|
|
||||||
|
/** Hibernate 5 style calls that use no persistence annotations. Compiled by scripts/compile-old-apis.sh against 5.6.15 and 7.4.14. */
|
||||||
|
@SuppressWarnings({"deprecation", "rawtypes"})
|
||||||
|
class OldHibernate {
|
||||||
|
|
||||||
|
Serializable save(Session s, Object o) {
|
||||||
|
return s.save(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
void update(Session s, Object o) {
|
||||||
|
s.update(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveOrUpdate(Session s, Object o) {
|
||||||
|
s.saveOrUpdate(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete(Session s, Object o) {
|
||||||
|
s.delete(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object load(Session s) {
|
||||||
|
return s.load(Object.class, 1L);
|
||||||
|
}
|
||||||
|
|
||||||
|
List criteria(Session s) {
|
||||||
|
Criteria c = s.createCriteria(Object.class).add(Restrictions.eq("name", "x"));
|
||||||
|
return c.list();
|
||||||
|
}
|
||||||
|
|
||||||
|
List sql(Session s) {
|
||||||
|
return s.createSQLQuery("select 1").list();
|
||||||
|
}
|
||||||
|
|
||||||
|
void flushMode(Session s) {
|
||||||
|
s.setFlushMode(FlushMode.AUTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object metadata(SessionFactory sf) {
|
||||||
|
return sf.getClassMetadata(Object.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user