Guide: Add Ebean Database Migration Generation to an Existing Maven Project
June 10, 2026 · View on GitHub
Purpose
This guide provides step-by-step instructions for adding Ebean DB migration generation to an existing Maven project that already uses Ebean ORM. Ebean generates migrations by performing a diff of the current entity model against the previously recorded model state, producing platform-specific DDL SQL scripts.
These instructions are designed for AI agents and developers to follow precisely.
Prerequisites
- An existing Maven project with Ebean ORM configured (entity beans present)
ebean-testis already a test-scoped dependency (from POM setup guide)- The project targets PostgreSQL (adjust
Platform.POSTGRESfor other databases)
Step 1 — Verify migration dependencies
Generation tooling (ebean-ddl-generator)
ebean-test (already present as a test dependency) transitively includes
ebean-ddl-generator, which provides the DbMigration class. No additional dependency
is required for generation.
Runtime migration runner (ebean-migration)
ebean-migration is the library that runs migrations on application startup.
It is typically included transitively via io.ebean:ebean-postgres (or the
equivalent platform dependency). Verify it is on the classpath by running:
mvn dependency:tree | grep ebean-migration
If it is not present transitively, add it explicitly as a compile-scope dependency:
<dependency>
<groupId>io.ebean</groupId>
<artifactId>ebean-migration</artifactId>
<version>${ebean.version}</version>
</dependency>
Step 2 — Create GenerateDbMigration.java
Create the following class in src/test/java/main/. This main method is run manually
by a developer (or AI agent) whenever entity beans change and a new migration is needed.
package main;
import io.ebean.annotation.Platform;
import io.ebean.dbmigration.DbMigration;
import java.io.IOException;
/**
* Generate the next database migration based on a diff of the entity model.
* Run this main method after making entity bean changes to produce the migration SQL.
*/
public class GenerateDbMigration {
public static void main(String[] args) throws IOException {
DbMigration migration = DbMigration.create();
migration.setPlatform(Platform.POSTGRES);
migration.setVersion("1.1"); // set to the next migration version
migration.setName("add-customer"); // short description of the change
migration.generateMigration();
}
}
Version naming convention
Ebean supports two common version formats — choose one and apply it consistently:
| Format | Example | Notes |
|---|---|---|
| Date-based | 20240820 | YYYYMMDD; used when changes are tied to dates; easily sortable |
| Semantic | 1.1, 1.2, 2.0 | Traditional versioning; useful for release-based workflows |
The version controls execution order — Ebean runs migrations in ascending version order.
Name convention
The name should be a short, lowercase, hyphenated description of the change:
add-customer-emailrename-machine-typedrop-unused-columns
Step 3 — Configure the output path (if needed)
By default, migration files are written to src/main/resources/dbmigration/ relative
to the current working directory when generateMigration() is called. This is
usually the module root, which is correct for single-module projects.
For multi-module projects where GenerateDbMigration is in a submodule but the
resources directory is at a different relative path, specify it explicitly:
// Relative path from the working directory (project root) to the module's resources
migration.setPathToResources("my-module/src/main/resources");
Step 4 — Run GenerateDbMigration to produce the first migration
Run the main method via the IDE or Maven:
# Run via Maven exec plugin (or use IDE run configuration)
mvn test-compile exec:java \
-Dexec.mainClass="main.GenerateDbMigration" \
-Dexec.classpathScope="test" \
-pl <your-module>
Ebean migration generation runs in offline mode — no database connection is required.
Expected output files
After running, two files are created per migration in src/main/resources/dbmigration/:
src/main/resources/dbmigration/
1.1__add-customer.sql ← DDL SQL to apply (commit this)
model/
1.1__add-customer.model.xml ← logical model diff XML (commit this)
Both files must be committed to source control. The .model.xml file records the
logical state of the diff and is used by subsequent migration generations to determine
what has changed.
If no entity beans have changed since the last migration, the command outputs:
DbMigration - no changes detected - no migration written
Step 5 — Enable the migration runner
Configure Ebean to run pending migrations automatically on application startup.
Preferred approach — programmatic via DatabaseBuilder
Set runMigration(true) directly on the DatabaseBuilder when constructing
the Database bean. This is the preferred approach as it is explicit, co-located with
the database configuration, and does not rely on external property files.
In the @Factory class that builds the Database bean (see the database configuration
guide), add .runMigration(true) to the builder chain:
@Bean
Database database(ConfigWrapper config) {
var dataSource = DataSourceBuilder.create()
.url(config.getDatabaseUrl())
.username(config.getDatabaseUser())
.password(config.getDatabasePassword())
// ... other datasource settings ...
;
return Database.builder()
.name("db")
.dataSourceBuilder(dataSource)
.runMigration(true) // run pending migrations on startup
.build();
}
If migrations should only run in certain environments (e.g., not in production, or only when a config flag is set), make it conditional:
.runMigration(config.isRunMigrations()) // driven by config value
Alternative — via application properties
If programmatic configuration is not available or not preferred, set the property
in src/main/resources/application.properties:
ebean.migration.run=true
Or in src/main/resources/application.yaml:
ebean:
migration:
run: true
For a named database (i.e., Database.builder().name("mydb")), use the database
name in the property key:
ebean.mydb.migration.run=true
What the runner does at startup
When migration running is enabled, Ebean will on each application start:
- Look at the migrations in
src/main/resources/dbmigration/ - Compare against the
db_migrationtable (created automatically on first run) - Apply any migrations that have not yet been executed, in version order
- Record each successfully applied migration in
db_migration
Step 6 — Commit the migration files
Add both generated files to source control:
git add src/main/resources/dbmigration/1.1__add-customer.sql
git add src/main/resources/dbmigration/model/1.1__add-customer.model.xml
git commit -m "Add db migration 1.1: add-customer"
Ongoing workflow — generating subsequent migrations
For each future set of entity bean changes:
- Make changes to the entity bean classes
- Update
GenerateDbMigration.javawith the new version and new name:migration.setVersion("1.2"); migration.setName("add-address-table"); - Run the
mainmethod — a new.sqland.model.xmlpair is written - Review the generated
.sqlto confirm it reflects the intended changes - Commit both files
Protecting hand-edited and non-versioned migrations across regeneration
GenerateDbMigration regenerates the apply SQL and model XML from the current
entity model. It can therefore overwrite content you did not change in the
entity beans, including:
- hand-edited DDL in a generated versioned
.sqlfile, and - repeatable (
R__*.sql) scripts that the generator also derives from the model (e.g. view definitions inextra-ddl.xml, built-in partitioning helpers).
Init scripts (I__*.sql) are write-once. If an init script already exists on
disk the generator does not rewrite it, so hand-tuned init DDL (partition
functions, UNLOGGED tables, triggers, seed data) is preserved across
regeneration. The trade-off: to pick up an upstream change to a built-in init
script (e.g. the partition helper) you must delete the file first, then
regenerate. Repeatable scripts are always regenerated.
To avoid losing manual work:
- Prefer an init (
I__) script for hand-maintained DDL the entity model cannot express — it is isolated and now protected from regeneration. - For versioned
.sqland repeatableR__scripts that the generator produces, review the diff after every regeneration and restore any clobbered hand-tuning (e.g.git checkout dbmigration/...) before committing. - If your build maintains a migration index file (e.g.
idx_*.migrations), re-check that the new migration is listed and filenames match after renaming a generated file.
Run the generator from the module directory. The output path set via
setPathToResources(...)is resolved relative to the working directory. RunGenerateDbMigrationwith the working directory set to the module that ownssrc/main/resources(e.g.cd serverfirst). Note thatmvn exec:javadoes not honour a configuredworkingDirectory, so set the cwd yourself.
Understanding the output files
Apply SQL (.sql)
The apply SQL file contains the DDL that will be executed against the database:
-- apply changes
alter table customer add column email varchar(255);
Model XML (.model.xml)
The model XML records the logical diff in a database-agnostic format. Ebean uses this file on the next generation run to determine what has already been captured. It is not executed against the database.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<migration xmlns="http://ebean-orm.github.io/xml/ns/dbmigration">
<changeSet type="apply">
<addColumn tableName="customer">
<column name="email" type="varchar(255)"/>
</addColumn>
</changeSet>
</migration>
Optional configurations
Multiple database platforms
To generate migrations for multiple platforms simultaneously, use addPlatform()
instead of setPlatform():
migration.addPlatform(Platform.POSTGRES);
migration.addPlatform(Platform.SQLSERVER17);
migration.addPlatform(Platform.MYSQL);
Each platform gets its own subdirectory under dbmigration/.
Include index
When enabled the migration generation also generates a file that contains all the migrations and their associated hashes. This is a performance optimisation (that will become the default) and means that the migration runner just needs to read the one resource and has the pre-computed hash values (so does not need to read each migration resource and compute the hash for each of those at runtime).
migration.setIncludeIndex(true);
Strict mode
Strict mode (on by default) errors if there are any pending drops not yet applied.
Set to false to allow generation to proceed regardless:
migration.setStrictMode(false);
Applying pending drops
Destructive changes (drop column, drop table) are not included in the apply
SQL by default — they are recorded as pendingDrops in the model XML. This allows
the application to be deployed without immediately dropping columns (important for
rolling deployments).
The migration runner logs a message when pending drops exist:
INFO DbMigration - Pending un-applied drops in versions [1.1]
When ready to apply the drops, set setGeneratePendingDrop to the version that
contains the pending drops:
migration.setVersion("1.3");
migration.setName("drop-pending-from-1.1");
migration.setGeneratePendingDrop("1.1"); // apply drops recorded in version 1.1
migration.generateMigration();
Custom dbSchema
If the project uses a named Postgres schema (set via ebean.dbSchema in
application.properties), no additional configuration is needed in
GenerateDbMigration — Ebean picks up the schema from the application config
automatically when running in offline mode.
# application.properties
ebean.dbSchema=myschema
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
no changes detected - no migration written | Entity beans unchanged since last migration | Make entity bean changes first, then re-run |
DbMigration - Pending un-applied drops | A previous migration has drops not yet applied | Either suppress with setStrictMode(false) or apply drops with setGeneratePendingDrop(...) |
| Generated SQL is empty or wrong | Wrong working directory path | Set setPathToResources(...) to the correct module-relative path |
ClassNotFoundException for entity classes | Test classpath not including main classes | Ensure exec.classpathScope=test or run via IDE with test classpath |
| Migrations not running on startup | Property key wrong or ebean-migration missing | Verify ebean[.name].migration.run=true and that ebean-migration is on the classpath |