Menghubungkan aplikasi Scala ke database: JDBC sebagai fondasi, Doobie untuk SQL pure functional dengan cats-effect, Quill dengan query compile-time, dan Slick sebagai FRM — ditambah HikariCP untuk connection pooling dan Flyway untuk migrasi skema.

Setelah di episode 13 kita membangun API dan di episode 14 memproses data, sekarang saatnya menutup sisi penyimpanan: database. Hampir semua aplikasi backend berakhir dengan menulis dan membaca data — dan cara Scala melakukannya adalah salah satu area dengan pilihan paling kaya di ekosistem JVM.
Mengapa episode ini penting? Karena pilihan akses database menentukan bentuk arsitektur kalian. Dari JDBC mentah hingga library pure functional seperti Doobie, masing-masing menukar kontrol dengan keselamatan dan produktivitas. Memahami peta ini membuat kalian memilih yang tepat untuk setiap proyek.
JDBC (Java Database Connectivity) adalah API standar Java untuk database — dan karena interop, ia tersedia langsung di Scala. Ini level terendah yang umum dipakai:
import java.sql.{Connection, DriverManager, PreparedStatement, ResultSet}
val url = "jdbc:postgresql://localhost:5432/mydb"
val conn: Connection = DriverManager.getConnection(url, "user", "pass")
val ps: PreparedStatement = conn.prepareStatement(
"SELECT id, name FROM users WHERE active = ?"
)
ps.setBoolean(1, true)
val rs: ResultSet = ps.executeQuery()
while rs.next() do
println(s"${rs.getInt("id")}: ${rs.getString("name")}")
rs.close(); ps.close(); conn.close()JDBC bekerja, tetapi semua boilerplate — koneksi, statement, result set, close — dikelola manual. Di produksi, kalian hampir selalu memakai lapisan di atasnya.
Membuat koneksi database itu mahal; pooling membuat ulang koneksi yang sudah dipakai. HikariCP adalah pool paling populer di ekosistem JVM:
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
val config = HikariConfig()
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb")
config.setUsername("user")
config.setPassword("pass")
config.setMaximumPoolSize(10)
val dataSource = HikariDataSource(config)
val conn = dataSource.getConnection()
try
// pakai koneksi
finally conn.close() // kembali ke pool, bukan ditutupPool membatasi concurrency (10 koneksi maksimal) dan mencegah DB kewalahan. Semua library di bawah ini memakai pooling di belakang layar.
Doobie adalah library SQL untuk cats-effect: query ditulis sebagai ekspresi nilai yang berjalan di effect, dengan type-check terhadap ResultSet. Tambahkan dependency:
libraryDependencies ++= Seq(
"org.tpolecat" %% "doobie-core" % "1.0.0-RC8",
"org.tpolecat" %% "doobie-postgres" % "1.0.0-RC8",
"org.tpolecat" %% "doobie-hikari" % "1.0.0-RC8"
)import cats.effect.{IO, IOApp}
import doobie.*
import doobie.implicits.*
import doobie.util.transactor.Transactor
case class User(id: Int, name: String)
object Main extends IOApp.Simple:
val xa = Transactor.fromDriverManager[IO](
"org.postgresql.Driver",
"jdbc:postgresql://localhost:5432/mydb",
"user", "pass"
)
val program: IO[List[User]] =
sql"SELECT id, name FROM users WHERE active = true"
.query[User]
.to[List]
val run: IO[Unit] =
program.flatMap(users => IO.println(users))Keunggulan Doobie:
case class type-safe terhadap kolom ResultSet.IO[Transaction] — keseluruhan semantics efek samping episode 11 berlaku.import doobie.free.connection.transact
val updateStock: ConnectionIO[Int] =
sql"UPDATE products SET stock = stock - 1 WHERE id = 42"
.update
.run
val insertOrder: ConnectionIO[Int] =
sql"INSERT INTO orders (product_id) VALUES (42)"
.update
.run
// satu transaksi: keduanya sukses atau tidak sama sekali
val tx: IO[Int] = (updateStock *> insertOrder).transact(xa)Semua query dalam satu program dijalankan dalam satu transaksi — atomic, konsisten, dan otomatis rollback jika gagal.
Tip
Untuk project berbasis cats-effect, Doobie adalah pilihan paling idiomatis: satu mental model (effect, transact, resource) dari ujung API sampai database. Stack-nya selaras penuh dengan http4s dan fs2 yang kita bahas sebelumnya.
Quill mengambil pendekatan berbeda: kalian menulis query dengan sintaks Scala (bukan string SQL), dan compiler menerjemahkannya menjadi SQL — artinya error query terdeteksi saat kompilasi, bukan runtime:
import io.getquill.*
import io.getquill.jdbczio.Quill
case class User(id: Int, name: String, active: Boolean)
object QuillExample:
val ctx = new PostgresJdbcContext(SnakeCase, "dbconfig")
import ctx.*
def findActive(): List[User] =
ctx.run(query[User].filter(u => u.active == true))Query ditulis dalam query[User].filter(...) — sebuah AST yang di-compile menjadi SQL string oleh Quill macro. Kesalahan tipe di filter langsung gagal di kompilasi.
Slick adalah FRM: tabel dipetakan ke objek Scala, query ditulis dengan API functional yang mirip collections:
import slick.jdbc.PostgresProfile.api.*
class Users(tag: Tag) extends Table[(Int, String, Boolean)](tag, "users"):
def id = column[Int]("id", O.PrimaryKey)
def name = column[String]("name")
def active = column[Boolean]("active")
def * = (id, name, active)
val users = TableQuery[Users]
val program: DBIO[Seq[(Int, String, Boolean)]] =
users.filter(_.active).resultSlick adalah salah satu library tertua dan paling matang, tetapi gaya FRM-nya kadang terasa "di antara" — bukan SQL murni (Doobie) dan bukan query compile-time penuh (Quill).
| Library | Gaya | Keunggulan | Ekosistem |
|---|---|---|---|
| JDBC | SQL mentah | Kontrol penuh | semua |
| Doobie | SQL sebagai effect | Type-safe, transaksi, cats-effect | Typelevel |
| Quill | Query dari sintaks Scala | Error saat kompilasi | ZIO-friendly |
| Slick | FRM, tabel sebagai objek | Matang, fitur lengkap | mandiri |
Skema database berubah seiring aplikasi. Flyway (dan alternatifnya Liquibase) mengelola migrasi sebagai file SQL bertanda versi:
src/main/resources/db/migration/
V1__create_users.sql
V2__add_email_column.sqlCREATE TABLE users (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE
);import org.flywaydb.core.Flyway
val flyway = Flyway.configure()
.dataSource("jdbc:postgresql://localhost:5432/mydb", "user", "pass")
.load()
flyway.migrate()Migrasi dijalankan sekali, dicatat di tabel flyway_schema_history, dan hanya file baru yang diterapkan. Ini menjadikan skema versi yang bisa di-deploy — salah satu praktik terbaik yang juga kita bahas dari sisi keamanan di episode 17.
Resource/try-finally; koneksi bocor adalah penyebab utama DB tak merespons.PreparedStatement / parameter query library.Inti yang harus dibawa pulang:
Di episode 16 selanjutnya, kita membahas penanganan kegagalan secara menyeluruh: error handling dan validation — Try, error handling effect, dan validation dengan ADT. Sampai jumpa di episode 16!