Exodus is a light, drop-in migration runner for Spring.
Exodus' aim is not to compete with incumbent migration runners on number of features, but rather to remove bloat and offer a simple solution to migrations in Spring applications.
- Download the latest
exodus.jar
fromdist/
above and place this JAR in your Spring project'ssrc/main/resources/lib/
directory. - Add Exodus as a dependency in your POM, taking care to set the version property as appropriate:
<dependency>
<groupId>com.albertomh</groupId>
<artifactId>exodus</artifactId>
<version>1.1.0</version>
<scope>system</scope>
<systemPath>${basedir}/src/main/resources/lib/exodus-1.1.0.jar</systemPath>
</dependency>
- Using the sample application entrypoint as a guide, do the following:
- Pass
scanBasePackages = {"com.albertomh.exodus"}
as a parameter to the@SpringBootApplication
decorator. - Instantiate an application context with
SpringApplication.run(YourApplication.class, args)
. - Call
applicationContext.start()
insidemain()
to emit aContextStartedEvent
. This is the cue for Exodus to run any pending migrations.
- Pass
These three steps are all that is needed to add Exodus to a project — you can now start writing migrations.
Exodus will pick up any .sql
files you place under src/main/resources/db/migration/
in your Spring application.
The following two best practices are recommended (but not enforced by Exodus):
- Subdivide
db/migration/
into directories named after the year the migrations they hold were written in. - Have migrations follow the naming convention
YYYY-MM-DD_HH.MM__<MODULE>__<CHANGE>.sql
where<MODULE>
is a subdivision of your app's functionality and<CHANGE>
is a concise summary of the change enacted by the migration. For instance:1970-01-01_09.00__auth__create-user.sql
.
The following line will appear in your Spring application's log the first time you run it after installing Exodus:
| exodus - Table `_schema_migration` has been created.
Exodus will create the table _schema_migration
to keep track of the migrations that have been applied:
id | applied_at | file_name | checksum |
---|---|---|---|
SERIAL PK |
TIMESTAMP |
VARCHAR(255) |
VARCHAR(128) |
If a valid migration is found within db/migration/
(at any depth, see above) _schema_migration
will be queried. If the migration is not listed in this table it will be applied and the following logged to the console:
| exodus - Migration `1970-01-01_09.00__auth__create-user.sql` has been applied.
After every run Exodus will log to the console a summary of operations taken:
| exodus - Ignored [5] existing migrations. Applied [2] new migrations.
After every run Exodus will publish a custom Spring event, the MigrationCompleteEvent
.
You can listen for this event in order to execute code after all migrations have been applied. Have your class implement ApplicationListener<MigrationCompleteEvent>
, overriding the onApplicationEvent
method:
import org.springframework.context.ApplicationListener;
import com.albertomh.exodus.event.MigrationCompleteEvent;
public class MyComponent implements ApplicationListener<MigrationCompleteEvent> {
@Override
public void onApplicationEvent(MigrationCompleteEvent event) {
// Your logic here.
}
}
Build Exodus with ./mvnw clean package
. This will create a JAR under target/
.
JARs should be placed under dist/
for new releases — avoid doing this manually and instead run the new_release.sh
script that takes care of this and other release-time tasks for you.
Tests are located under src/test/
and laid out in the way common to Java projects, replicating the file structure of the application source code.
Verify any changes you make by running ./mvnw clean test
(suite of unit & integration tests) from the project root.
- Merge all changes into the
main
branch and update the<version>
property in the POM. - Run
new_release.sh
. This will run all tests, create a new JAR & place it indist/
, and update README badges. - Commit the new JAR and update
RELEASES.md
. - Tag the new release in git and push to origin.
Exodus makes use of components in org.springframework.core
, o.s.context
, o.s.util
, and o.s.jdbc
.
At compile-time these are provided by the spring-boot-starter-web
& spring-boot-starter-data-jpa
libraries.
At run-time these are not provided as part of an uberJAR since Exodus is designed to be a dependency of a Spring application and as such will make use of the Spring libraries available in its run-time context.
In a test context two further dependencies — spring-boot-starter-test
& the H2
database — are required. The former provides testing libraries such as JUnit5, while the latter is an in-memory database used to quickly build and tear down databases during unit and integration testing.
- A datasource is injected into
MigrationRunner
's constructor and this sets up a connection and statement for use throughout the Exodus instance's lifetime. getMigrationScripts()
fetches migration scripts from the resources directory.createSchemaMigrationTable()
creates the_schema_migration
table if one does not already exist.onApplicationEvent()
is Exodus' main loop, invoked when aContextStartedEvent
is emitted by the Spring application on startup. It will create the table in which migrations are recorded, loop through every migration script & assess whether it needs to be applied, and log appropriate messages to the console.
A collection of utilities to interact with the database (more accurately, the DataSource
injected into the MigrationRunner
). Provides ways to inspect the tables in the database, list applied migrations, and apply a pending migration.
This module provides utilities used exclusively by the test suite.
createSchemaMigrationTable()
creates the_schema_migration
table as part of setting up unit tests so that they don't depend on the same functionality implemented in theMigrationRunner
.addRowToSchemaMigrationTable()
simulates the behaviour of the runner by appending a row with arbitrary data to the_schema_migration
table.
A suite of tests for Exodus' main loop. A utility creates synthetic ContextStartedEvent
s to simulate a Spring application's starting up.
Unit tests check methods that fetch migration scripts from the resources directory or create the _schema_migration
table. Integration tests assess the main loop's functionality under different conditions (blank database, previously-applied migrations, etc.).
A suite of tests for the DatabaseUtils
tools. Unit tests verify simple functions such as counting the number of tables or listing applied migrations. Integration tests check the functionality of more complex procedures such as applying a migration.
For both test suites:
- The constructor sets up a new ephemeral H2 datasource, connection, and statement when a suite is instantiated.
beforeEach()
drops all objects in the H2 test database to make each test independent.
Test resources are provided under src/test/resources/db/migration/
, mimicking their location in a production application using Exodus. Two migrations are available to tests under the 1970/
subdirectory.
Copyright 2022 Alberto Morón Hernández
This software is provided as open-source under the MIT License.