A lightweight, simple and functional wrapper for Flyway using cats-effect.
The most famous library to handle database migrations in Java is for sure Flyway. It works very well and the community edition has a lot of features as well. But Flyway APIs are written in the standard OOP paradigm, so throwing exceptions, manually managing resources, etc…
Fly4s is a lightweight, simple and functional wrapper for Flyway.
The aim of
Fly4s is straightforward, wrapping the
Flyway APIs to guarantee
referential transparency, pureness, resource handling and type safety.
To achieve this goal,
Fly4s use the typelevel libraries
- Getting started
- Migrations files
- Defining database configuration
- Instantiating Fly4s
- Using Fly4s
Fly4s supports Scala 2.13 and 3.
The first step, import the
Fly4s library in our SBT project.
So, add the dependency in your
Fly4s depends on Flyway, so we’ll have access to Flyway as well
libraryDependencies += "com.github.geirolz" %% "fly4s-core" % "0.0.13"
As the plain Flyway, we have to create a folder that will contain our migrations scripts, often in
In this folder, we have to put all our migration. We can have:
For this example, we are going to use a simple
baseline migration to add a table to our database schema.
Baseline migrations are versioned and executed only when needed. The version is retrieved from the script file name.
So in this case,
V__001_create_user_table.sql, the version will be
001(remember the double underscore after
Here we have our first migration(for MySQL database)
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT PRIMARY KEY, `name` varchar(30) NOT NULL, `surname` varchar(30) NOT NULL );
Defining database configuration
A good practice is to create a case class to handle the database configuration(this combined with PureConfig or others config libraries make your app very robust from the configuration point of view)
Let’s create a simple case class to achieve this.
case class DatabaseConfig( url: String, user: Option[String], password: Option[Array[Char]], migrationsTable: String, migrationsLocations: List[String] )
N.B. apart from the common fields such
password we’ll use:
migrationsTable to define the
Flyway table name(used to store the migration status) and
migrationsLocations to specify a list
of the folders that contain our migration scripts.
Ok so, now we have all our migration scripts in our folder(
resources/db), we have
Fly4s as a dependency
of our project,
and we have a case class that will contain the database configuration.
Fly4s we can use
make to create a new DataSource(under the hood) starting from the parameters
makeFor in order to create it for an already existent
DataSource(for example from Doobie HikariDataSource).
makeFor method returns a
Resource type class
that when released/interrupted safely close the
makeFor methods, we can specify the parameter
Fly4sConfig is a trivial wrapper for
Configuration but instead of having a builder we have a case class.
import fly4s.core.* import fly4s.core.data.* import cats.effect.* val dbConfig: DatabaseConfig = DatabaseConfig( url = "url", user = Some("user"), password = None, migrationsTable = "flyway", migrationsLocations = List("db") ) // dbConfig: DatabaseConfig = DatabaseConfig( // url = "url", // user = Some(value = "user"), // password = None, // migrationsTable = "flyway", // migrationsLocations = List("db") // ) val fly4sRes: Resource[IO, Fly4s[IO]] = Fly4s.make[IO]( url = dbConfig.url, user = dbConfig.user, password = dbConfig.password, config = Fly4sConfig( table = dbConfig.migrationsTable, locations = Location.of(dbConfig.migrationsLocations) ) ) // fly4sRes: Resource[IO, Fly4s[IO]] = Allocate( // resource = cats.effect.kernel.Resource$$$Lambda$12147/778703545@44914209 // )
Ok, we have done with the configuration! We are ready to migrate our database schema with the power of Flyway and the safety of Functional Programming!
We can use
Resource to safely access to the Fly4s instance. In case we have
Resources in our application probably
evalMap allow us to better combine them using and releasing
them all together at the same time.
We can create a simple util method to do this
import fly4s.implicits.* def migrateDb(dbConfig: DatabaseConfig): Resource[IO, MigrateResult] = Fly4s.make[IO]( url = dbConfig.url, user = dbConfig.user, password = dbConfig.password, config = Fly4sConfig( table = dbConfig.migrationsTable, locations = Location.of(dbConfig.migrationsLocations) ) ).evalMap(_.validateAndMigrate.result)
We have done it! So, to recap, we have:
- Created a folder under
resourcesto put our migrations(
Fly4sas a dependency in our project
- Created a configuration case class to describe our database configuration
- Instantiated a
Fly4sinstance creating a new
- Migrated our database using
- At the application shutdown/interruption
Resource(from cats-effect) will safely release the
With a few lines, we have migrated our database safely handling the connection and the configuration.
As flyway, Fly4s provides multiple methods such as: