CI/CD Databases with Flyway

A solution for continuous database integration

Jonathan Manera
6 min readAug 4, 2022
Image by Jackeisley on Wikipedia

When working for large organizations, we developers do not usually deploy our own applications. Therefore, we may need to leverage a migration tool to continuously integrate and deploy database schemas along with the rest of the new code.

In this article, we will explore how migration tools like Flyway can help us promote software releases in different environments easily and reliably.

Migration tools vs Hibernate ddl-auto

If you have some professional experience, the difference between a migration tool and Hibernate might seem rather obvious. But many students keep asking me “why not use ddl-auto instead?”

The spring.jpa.hibernate.ddl-auto property can be used to perform DDL operations based on the current object model mapping (typically JPA Entities) when the application starts.

Hibernate provides four property values to this end: none, validate, create-drop and update. Spring internally chooses a default value for this property: create-drop if we are using an embedded database, or none otherwise.

We are not digging deeper into these four properties today. What we are interested in knowing is that we can use the property value update to compare the current object model with the existing schema, and Hibernate will update the schema accordingly.

Although this may sound good enough, there are certain drawbacks:

  • Hibernate does not store the change log, making it difficult to know what was changed between deployments or to perform rollbacks.
  • In Hibernate, it becomes challenging to use vendor-specific database features without sacrificing application portability.
  • Hibernate does not delete existing tables or columns, even if they are no longer needed by the application.

How does Flyway work?

Migration Metadata

Flyway creates a special table called the “Flyway Schema History” to store metadata, such as script name, migration version, execution status, execution date-time, etc.

Then, to keep track of the schema evolution, Flayway logs the metadata into the flyway_schema_history table, leaving this table as the only source of truth, and allowing it to check if a new migration script has been added.

In addition, it validates the integrity of older scripts using a checksum stored in the table. So, if any of the scripts in the table has been modified, the application will fail to start.

Script Naming Convention

In order to continuously add new migration scripts, Flyway enforces a naming convention:

<Prefix><Version>__<Name>.sql

Where:

  • <Prefix> stands for the migration prefix. By default, it is V.
  • <Version> stands for the migration version number. It can be divided into major and minor versions based on the Semantic Versioning recommendation: MAJOR.MINOR.
  • <Name> stands for the basic description of the migration. Note that a double underscore separates the name from the version number.

Example:

V1.0__add_tables.sql
V1.1__add_data.sql

Flyway on Spring

Flyway Core Dependency

To enable Flyway on Spring, all we need to do is add the flyway-core dependency. If you are working on a Maven project, you will need the following dependency in your pom.xml file.

<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

With this dependency, Flyway automatically scans the classpath looking for the migration scripts, and executes them against the database during application startup. By default, it will scan for files located in the path classpath:db/migration, but you can modify that location by setting the path in the spring.flyway.locations property.

Spring Configuration

First, add the connection to the database in yourapplication.yml file. In this example, we are using a H2 database, but Flyway is compatible with all popular databases: MySQL, Oracle, Postgres and so forth.

spring:  
datasource:
driver-class-name: org.h2.Driver
username: sa
password:
url: "jdbc:h2:mem:db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_LOWER=TRUE;CASE_INSENSITIVE_IDENTIFIERS=TRUE"
h2:
console:
enabled: true
path: /h2-console
jpa:
show-sql: true
hibernate:
ddl-auto: none

Additionally, we are setting the property spring.jpa.hibernate.ddl-auto as none.

Database Migration

Let’s add the first migration script V1.0__add_tables.sql into classpath:db/migration.

drop table if exists trainee;
drop table if exists skill;
drop table if exists trainee_skill;

create table trainee
(
trainee_id bigint not null auto_increment primary key,
start_date datetime(6) not null,
end_date datetime(6) null,
name varchar(36) not null
);

create table skill
(
skill_id bigint not null primary key,
description varchar(255) null,
name varchar(36) null
);

create table trainee_skill
(
trainee_id bigint not null,
skill_id bigint not null,
primary key (trainee_id, skill_id),
constraint trainee_skill_trainee_id_fk
foreign key (trainee_id) references trainee (trainee_id),
constraint trainee_skill_skill_id_fk
foreign key (skill_id) references skill (skill_id)
);

Here, we define the DDL operations and create three tables: trainee, skill and trainee_skill.

Now that we have the DDL for the tables, we define the object model mapping.

In the code below, you’ll see two JPA Entities: Trainee and Skill.

import java.time.LocalDateTime
import javax.persistence.*

@Entity
@Table(name = "trainee")
open class Trainee {

@get:Id
@get:GeneratedValue
@get:Column(name = "trainee_id")
open var id: Long? = null

@get:Column(nullable = false)
open var name: String? = null

@get:Column(nullable = false)
open var startDate: LocalDateTime? = null

open var endDate: LocalDateTime? = null

@get:ManyToMany
@get:JoinTable(
name = "trainee_skill",
joinColumns = [JoinColumn(name = "trainee_id")],
inverseJoinColumns = [JoinColumn(name = "skill_id")]
)
open var skills: Set<Skill> = HashSet()
}

@Entity
@Table(name = "skill")
open class Skill {

@get:Id
@get:Column(name = "skill_id")
open var id: Long? = null

@get:Column(nullable = false)
open var name: String? = null

open var description: String? = null
}

Once the application has raised, we can access to the H2 Console (http://localhost:<port>/h2-console) with the credentials bellow:

  • JDBC URL: jdbc:h2:mem:db
  • User Name: sa
  • Password: <empty>

By querying the flyway_schema_history_table, we can see that the script was indeed executed, and the schema was created.

Database Seeding

When deploying a new application, it is common to load seed data, such as user accounts, into the initial setup of the application.

Let’s add the seed script V1.1__add_data.sql into classpath:db/migration.

insert into skill(skill_id, description, name)
values (1, 'Domain-specific language for managing data in relational database management systems.', 'SQL');
insert into skill(skill_id, description, name)
values (2, 'Java based programing language for developing web applications.', 'Kotlin');
insert into skill(skill_id, description, name)
values (3, 'Spring framework for building web applications on top of the Java EE platform.', 'Spring');
insert into skill(skill_id, description, name)
values (4, 'ORM tool for mapping an object-oriented domain model to a relational database.', 'Hibernate');

insert into trainee(trainee_id, start_date, name)
values (1, current_timestamp(), 'Joe Friend');
insert into trainee(trainee_id, start_date, name)
values (2, current_timestamp(), 'Juan Amigo');
insert into trainee(trainee_id, start_date, name)
values (3, current_timestamp(), 'Jane Fresh');
insert into trainee(trainee_id, start_date, name)
values (4, current_timestamp(), 'Joana Fresco');

insert into trainee_skill values (1, 1);
insert into trainee_skill values (1, 2);
insert into trainee_skill values (2, 2);
insert into trainee_skill values (2, 3);
insert into trainee_skill values (3, 3);
insert into trainee_skill values (3, 4);
insert into trainee_skill values (4, 1);
insert into trainee_skill values (4, 2);
insert into trainee_skill values (4, 3);
insert into trainee_skill values (4, 4);

Now that we have the DML to populate data, let’s launch the application. No further configuration is necessary.

By querying the flyway_schema_history_table table, we can see that the new script was executed.

Similarly, we can see that the tables have been populated with the seed.

Summary

In this article, we learned how a migration tool like Flyway can help us to continuously integrate and deploy database schemas easily and reliably.

Thanks for reading. I hope this was helpful!

The example code is available on GitHub.

--

--

Jonathan Manera

If you wish to make a Java app from scratch, you must first invent the universe.