Database Migrations in Kotlin: A Complete Guide to Tools and Approaches
Managing database schema changes is one of the key tasks when developing any application. In the Kotlin ecosystem, there are several mature solutions that allow you to automate this process. In this article, we will cover three main approaches: built-in ORM Exposed tools, the classic Flyway, and the powerful Liquibase. You will learn how to integrate them into a Kotlin project and which tool to choose for specific tasks.
1. Why are migrations needed?
Manually executing SQL scripts in different environments (local development, staging, production) inevitably leads to schema desynchronization. Migrations solve this problem by providing:
- Versioning of database changes
- Automatic application of new scripts when the application starts
- The ability to rollback to a previous version
- Team collaboration without schema conflicts
2. Exposed: Native Migration Support
Exposed is a lightweight ORM from JetBrains written in Kotlin. Starting from version 0.41.0, the library provides its own migration mechanism that fits perfectly into the Kotlin ecosystem.
Installation
// build.gradle.ktsimplementation("org.jetbrains.exposed:exposed-core:0.55.0")implementation("org.jetbrains.exposed:exposed-dao:0.55.0")implementation("org.jetbrains.exposed:exposed-migrations:0.55.0")Creating a Migration
Each migration is a class that inherits from VoidDatabaseMigration:
import org.jetbrains.exposed.sql.transactions.transactionimport org.jetbrains.exposed.sql.SchemaUtilsimport org.jetbrains.exposed.sql.Tableimport org.jetbrains.exposed.migrations.VoidDatabaseMigration
object Users : Table() { val id = integer("id").autoIncrement() val name = varchar("name", 255) override val primaryKey = PrimaryKey(id)}
class V1__CreateUsersTable : VoidDatabaseMigration() { override fun migrate() { transaction { SchemaUtils.create(Users) } }}
class V2__AddEmailColumn : VoidDatabaseMigration() { override fun migrate() { transaction { SchemaUtils.addColumn(Users, Users.varchar("email", 255)) } }}Running Migrations
import org.jetbrains.exposed.migrations.ExposedMigrationUtilsimport javax.sql.DataSource
fun runMigrations(dataSource: DataSource) { ExposedMigrationUtils.runMigrations( dataSource = dataSource, migrationsPackage = "com.example.migrations" )}Pros: fully Kotlin-style, no SQL files, typed column names.
Cons: tied to Exposed, no visual tracker, no rollback out of the box.
3. Flyway: Simplicity and Reliability
Flyway is the most popular migration tool in the JVM world. It works with any database and is independent of ORM. In Kotlin, it is used via the official SDK or Gradle plugin.
Setup
// build.gradle.ktsimplementation("org.flywaydb:flyway-core:10.20.0")// For PostgreSQL, add the driver:implementation("org.postgresql:postgresql:42.7.4")Migration Structure
Flyway uses SQL files with specific naming:
db/migration/ V1__create_users.sql V2__add_email_column.sql V3__add_role_column.sqlExample SQL Migration (V1__create_users.sql)
CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) UNIQUE);Integration with Kotlin
import org.flywaydb.core.Flywayimport javax.sql.DataSou