Belajar Scala - Database & Persistence
Episode 15 of 23

Belajar Scala - Database & Persistence

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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: Fondasi Semua Akses DB

JDBC (Java Database Connectivity) adalah API standar Java untuk database — dan karena interop, ia tersedia langsung di Scala. Ini level terendah yang umum dipakai:

JDBC dasar di Scala
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.

Connection Pooling: HikariCP

Membuat koneksi database itu mahal; pooling membuat ulang koneksi yang sudah dipakai. HikariCP adalah pool paling populer di ekosistem JVM:

HikariCP di Scala
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 ditutup

Pool membatasi concurrency (10 koneksi maksimal) dan mencegah DB kewalahan. Semua library di bawah ini memakai pooling di belakang layar.

Doobie: SQL Pure Functional

Doobie adalah library SQL untuk cats-effect: query ditulis sebagai ekspresi nilai yang berjalan di effect, dengan type-check terhadap ResultSet. Tambahkan dependency:

Dependency Doobie
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"
)
Query Doobie
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:

  • Query adalah nilai — bisa dikomposisi, dibatalkan, diuji.
  • Mapping ke case class type-safe terhadap kolom ResultSet.
  • Transaksi adalah IO[Transaction] — keseluruhan semantics efek samping episode 11 berlaku.

Transaksi dengan Doobie

Transaksi Doobie
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: Query Compile-Time

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:

Quill - query compile-time
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: FRM (Functional Relational Mapping)

Slick adalah FRM: tabel dipetakan ke objek Scala, query ditulis dengan API functional yang mirip collections:

Slick FRM
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).result

Slick 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).

LibraryGayaKeunggulanEkosistem
JDBCSQL mentahKontrol penuhsemua
DoobieSQL sebagai effectType-safe, transaksi, cats-effectTypelevel
QuillQuery dari sintaks ScalaError saat kompilasiZIO-friendly
SlickFRM, tabel sebagai objekMatang, fitur lengkapmandiri

Migrasi Skema dengan Flyway

Skema database berubah seiring aplikasi. Flyway (dan alternatifnya Liquibase) mengelola migrasi sebagai file SQL bertanda versi:

Struktur migrasi Flyway
src/main/resources/db/migration/
  V1__create_users.sql
  V2__add_email_column.sql
V1__create_users.sql
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  active BOOLEAN NOT NULL DEFAULT TRUE
);
Jalankan migrasi saat startup
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.

Common Pitfalls

  • Koneksi tidak di-close — selalu pakai pool (HikariCP) dan Resource/try-finally; koneksi bocor adalah penyebab utama DB tak merespons.
  • SQL injection — jangan pernah merangkai string SQL dengan input user; selalu PreparedStatement / parameter query library.
  • N+1 query — memuat relasi satu-per-satu di loop; pelajari join dan batching.
  • Migrasi diubah setelah dijalankan — sekali diterapkan, file migrasi tidak boleh diedit; buat file V3 baru.

Penutup

Inti yang harus dibawa pulang:

  • JDBC adalah fondasi; HikariCP menyediakan connection pooling.
  • Doobie = SQL sebagai effect cats-effect dengan transaksi type-safe — pilihan paling idiomatis di stack Typelevel.
  • Quill memindahkan error query ke compile-time; Slick memetakan tabel sebagai objek.
  • Flyway mengelola migrasi skema sebagai file versi yang bisa di-deploy.
  • Keselamatan data: pakai parameter query, pool, dan transaksi.

Di episode 16 selanjutnya, kita membahas penanganan kegagalan secara menyeluruh: error handling dan validationTry, error handling effect, dan validation dengan ADT. Sampai jumpa di episode 16!

Belajar Scala - Database & Persistence | Belajar Scala