Automatic migration planning for Hibernate
TL;DR
- Hibernate is an ORM library that's widely used in the Java community.
- Atlas is an open-source tool for inspecting, planning, linting and executing schema changes to your database.
- Developers using Hibernate can use Atlas to automatically plan schema migrations for them, based on the desired state of their schema instead of crafting them by hand.
Automatic migration planning for Hibernate
Hibernate is a popular ORM widely used in the Java community. Hibernate allows users to manage their database schemas using its automatic schema generation feature, which is usually sufficient during development and in many simple cases.
However, at some point, teams need more control and decide to employ the versioned migrations methodology, which is a more robust way to manage your database schema. Once this happens, the responsibility for planning migration scripts and making sure they are in line with what Hibernate expects at runtime is moved to developers.
Atlas can automatically plan database schema migrations for developers using Hibernate. Atlas plans migrations by calculating the diff between the current state of the database and its desired state.
In the context of versioned migrations, the current state can be thought of as the database schema that would have been created by applying all previous migration scripts.
The desired schema of your application can be provided to Atlas via an External Schema Data Source, which is any program that can output a SQL schema definition to stdout.
To use Atlas with Hibernate, users can utilize the Hibernate Atlas Provider, a small program that can be used to load the schema of a Hibernate project into Atlas.
In this guide, we will show how Atlas can automatically plan schema migrations for Hibernate users.
Prerequisites
- A local Hibernate project.
If you don't have a Hibernate project handy, check out the Hibernate getting started page
Using the Atlas Hibernate Provider
In this guide, we will use the Hibernate Atlas Provider to automatically plan schema migrations for a Hibernate project.
Using the provider does not require a connection to a live database.
Installation
Install Atlas from macOS or Linux by running:
curl -sSf https://atlasgo.sh | sh
See atlasgo.io for more installation options.
Install the provider by adding this plugin to your Gradle project:
- Gradle Kotlin
- Gradle Groovy
- Maven
plugins {
id("io.atlasgo.hibernate-provider-gradle-plugin") version "0.1"
}
plugins {
id "io.atlasgo.hibernate-provider-gradle-plugin" version "0.1"
}
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>io.atlasgo</groupId>
<artifactId>hibernate-provider-maven-plugin</artifactId>
<version>0.1</version>
</plugin>
</plugins>
</pluginManagement>
</build>
Verify that the plugin is installed by running this command:
- Gradle
- Maven
./gradlew help --task schema
mvn help:describe -Dplugin=hibernate-provider -Dgoal=schema
Library vs Standalone
The Atlas Hibernate Provider can be used in two modes:
- Standalone - In most cases, you can use the provider as a Gradle task (or a Maven Mojo) to output the Hibernate schema and load the schema into Atlas.
- Library - If you are initializing Hibernate in a custom way, using an older version of Hibernate or if you are not using Gradle or Maven, you can use the provider as a library and create a small application that prints the schema to stdout.
Standalone
In your project directory, create a new file named atlas.hcl
with the following contents:
- Gradle
- Maven
data "external_schema" "hibernate" {
program = [
"./gradlew",
"-q",
"schema",
"--properties", "schema-export.properties"
]
}
data "external_schema" "hibernate" {
program = [
"./mvn",
"-q",
"hibernate-provider:schema",
"-Dproperties", "schema-export.properties"
]
}
Depending on your database, add an environment configuration. For example, for MySQL and PostgreSQL:
- MySQL
- PostgreSQL
env "hibernate" {
src = data.external_schema.hibernate.url
dev = "docker://mysql/8/dev"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}
env "hibernate" {
src = data.external_schema.hibernate.url
dev = "docker://postgres/15/dev?search_path=public"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}
We need to let Atlas and the provider know which SQL dialect should be used. The dialect is specified in the properties file and in the Dev Database that Atlas will use.
Create a resource named schema-export.properties
in src/main/resource
, it should be part of the classpath by default.
Specify the SQL dialect that will be used to initialize Hibernate:
- MySQL
- PostgreSQL
jakarta.persistence.database-product-name=MySQL
jakarta.persistence.database-major-version=8
jakarta.persistence.database-product-name=PostgreSQL
Note: The properties being used might vary depending on the version of Hibernate and the database connector that is configured for your project.
Library mode
We are going to add the provider as a dependency in Gradle (or Maven) and configure Hibernate to use
ConsoleSchemaManagementTool
. In this example, we are going to use Spring to initialize Hibernate.
By default, the
HibernateSchemaManagementTool
will try to connect to the database during initialization and generate the schema into the database. TheConsoleSchemaManagementTool
overrides this behaviour.
Start by adding the provider as a dependency:
- Gradle
- Maven
dependencies {
implementation("io.atlasgo:hibernate-provider:0.1")
}
<dependency>
<groupId>io.atlasgo</groupId>
<artifactId>hibernate-provider</artifactId>
<version>0.1</version>
</dependency>
Create a new file named HibernateSchemaExporter.java
with the following contents:
@SpringBootApplication
@PropertySource(value = {"classpath:schema-export.properties"})
public class HibernateSchemaExporter {
public static void main(String[] args) {
new AnnotationConfigApplicationContext(HibernateSchemaExporter.class);
}
}
Next, create a new resource named schema-export.properties
(make sure it is in the classpath) with this content:
- MySQL
- PostgreSQL
spring.jpa.properties.jakarta.persistence.database-product-name=MySQL
spring.jpa.properties.jakarta.persistence.database-major-version=8
spring.jpa.properties.jakarta.persistence.schema-generation.database.action=create
spring.jpa.properties.hibernate.schema_management_tool=io.atlasgo.ConsoleSchemaManagementTool
spring.jpa.properties.jakarta.persistence.database-product-name=PostgreSQL
spring.jpa.properties.jakarta.persistence.schema-generation.database.action=create
spring.jpa.properties.hibernate.schema_management_tool=io.atlasgo.ConsoleSchemaManagementTool
Lastly, in your project directory, create a new file named atlas.hcl
with the following contents:
data "external_schema" "hibernate" {
program = [
"./gradlew",
"-q",
"bootRun"
]
}
Depending on your database, add an environment configuration. For example, for MySQL and PostgreSQL:
- MySQL
- PostgreSQL
env "hibernate" {
src = data.external_schema.hibernate.url
dev = "docker://mysql/8/dev"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}
env "hibernate" {
src = data.external_schema.hibernate.url
dev = "docker://postgres/15/dev?search_path=public"
migration {
dir = "file://migrations"
}
format {
migrate {
diff = "{{ sql . \" \" }}"
}
}
}
Usage
Atlas supports a versioned migrations
workflow, where each change to the database is versioned and recorded in a migration file. You can use the
atlas migrate diff
command to automatically generate a migration file that will migrate the database
from its latest revision to the current Hibernate schema.
Suppose we have the following Hibernate com.example.model
package, with two models Event
and Location
:
- Event.java
- Location.java
package com.example.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import static jakarta.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "Events")
public class Event {
private String title;
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
}
package com.example.model;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import static jakarta.persistence.GenerationType.IDENTITY;
@Entity
@Table(name = "Events")
public class Location {
@Id
@GeneratedValue(strategy = IDENTITY)
private Long id;
}
Atlas uses the atlas.sum
file to protect against conflicting schema changes, you can read about it here.
Currently, Atlas does not support using generated fields that require data initialization such as GenerationType.SEQUENCE
, GenerationType.TABLE
, and Generation.AUTO
.
If needed, you can still export the schema using the flag --enable-table-generators (or -Denable-table-generators using Maven). When applying the schema to your database, you will need
to make sure to apply the ignored statements (using atlas migrate --env hibernate diff --edit
). See more information on manual migrations here
For example, if you are adding GenerationType.SEQUENCE to the Event entity, you will need to add insert statements to your generated migration file:
diff --git a/migrations/20231210140844.sql b/examples/with_local_plugin_repository/migrations/20231210140844.sql
index ad80a64..5955834 100644
--- a/migrations/20231210140844.sql
+++ b/migrations/20231210140844.sql
@@ -4,3 +4,6 @@ CREATE TABLE `Event` (`id` bigint NOT NULL AUTO_INCREMENT, `title` varchar(255)
-- Create "Event_SEQ" table
CREATE TABLE `Event_SEQ` (`next_val` bigint NULL) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+ -- Initialize "Event_SEQ" table
+ insert into Event_SEQ values ( 1 );
Testing these changes can be done by running the application with a local database and creating the entity. To apply the migration directory
to the local database, use atlas migrate apply
.
Using the configuration we created earlier, we can generate a migration file by running the following command:
atlas migrate diff --env hibernate
Running this command will generate files similar to this in the migrations
directory:
migrations
|-- 20231128110831.sql
`-- atlas.sum
0 directories, 2 files
Examining the contents of 20231128110831.sql
:
-- Create "Events" table
CREATE TABLE `Events` (
`id` bigint NOT NULL AUTO_INCREMENT,
`title` varchar(255) NULL,
PRIMARY KEY (`id`)
) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Create "Locations" table
CREATE TABLE `Locations` (
`id` bigint NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
Amazing! Atlas automatically generated a migration file that will create the Events
and Locations
tables in our database!
Next, alter the Event
model to add a new Location
field:
import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
@Entity
@@ -11,6 +12,9 @@ import jakarta.persistence.Table;
public class Event {
private String title;
+ @ManyToOne
+ private Location location;
+
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
Re-run this command:
atlas migrate diff --env hibernate
Observe that a new migration file is generated:
-- Modify "Events" table
ALTER TABLE `Events` ADD COLUMN `location_id` bigint NULL, ADD INDEX `FK4hncpre5frbs4krenagj1xyk4` (`location_id`), ADD CONSTRAINT `FK4hncpre5frbs4krenagj1xyk4` FOREIGN KEY (`location_id`) REFERENCES `Locations` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION;
Conclusion
In this guide we demonstrated how projects using Hibernate can use Atlas to automatically
plan schema migrations based solely on their data model. To learn more about executing
these migrations against your production database, read the documentation for the
migrate apply
command.
Have questions? Feedback? Find our team on our Discord server