Repo dan konfigurasi

Postgres, sandbox pengujian, dan koneksi Repo.

Daftar isi

Repo adalah process yang menjadi satu-satunya pintu masuk ke database pada aplikasi Elixir yang menggunakan Ecto: seluruh pembacaan dan penulisan data melewati fungsi-fungsinya. Topik ini membahas konfigurasi Repo, pembukaan koneksi pertama ke PostgreSQL, query, yaitu perintah yang dikirim ke database melalui Repo.query!/2, serta persiapan sandbox pengujian yang membuat setiap tes berjalan pada data yang selalu kembali bersih. Seluruh contoh dijalankan terhadap server Postgres di 127.0.0.1 port 5433 dengan pengguna postgres tanpa kata sandi.

Repo sebagai gateway database#

Repo adalah modul yang membungkus pool, yaitu kumpulan koneksi database yang dipinjam bergantian oleh pemanggil, dan menawarkan antarmuka akses data yang seragam. Melalui antarmuka tersebut, Repo berperan sebagai gateway, yakni satu titik lalu lintas data menuju database. Modul Repo didefinisikan dengan use Ecto.Repo yang menerima dua informasi penting, yaitu otp_app sebagai nama aplikasi tempat konfigurasi disimpan dan adapter sebagai driver database yang digunakan. Definisi berikut adalah bentuk yang dihasilkan generator Phoenix untuk sebuah proyek bernama perpustakaan:

elixir Definisi modul Repo pada proyek Mix
# tidak-dijalankan
defmodule Perpustakaan.Repo do
  use Ecto.Repo,
    otp_app: :perpustakaan,
    adapter: Ecto.Adapters.Postgres
end

use Ecto.Repo tidak menyimpan nilai konfigurasi apa pun di dalam kode. Nilai konfigurasinya dibaca dari Application environment, yaitu penyimpanan pengaturan yang berjalan bersama VM, dengan kunci otp_app dan nama modul Repo. Pada proyek Mix, nilai itu ditulis di berkas konfigurasi:

elixir Konfigurasi Repo pada config/config.exs
# tidak-dijalankan
import Config

config :perpustakaan, Perpustakaan.Repo,
  database: "perpustakaan_dev",
  username: "postgres",
  password: "postgres",
  hostname: "localhost"

Berkas config/config.exs memuat pengaturan yang sama untuk semua lingkungan, sedangkan nilai yang berbeda antarlingkungan ditempatkan pada config/dev.exs, config/test.exs, dan config/prod.exs. Skrip pada modul ini berjalan tanpa proyek Mix, sehingga konfigurasi yang sama disetel secara programatik melalui Application.put_env/3 sebelum modul Repo didefinisikan. Karena kunci dan nilainya sama persis, pola konfigurasinya tetap dapat dipindahkan ke proyek sungguhan.

Konfigurasi koneksi dan pool#

Setiap kunci konfigurasi mengatur satu aspek koneksi, yaitu alamat server, identitas pengguna, nama database, dan jumlah koneksi yang dipelihara. Tabel berikut merangkum kunci yang paling sering disetel:

KunciPeran
hostnameAlamat server database, misalnya 127.0.0.1.
portPort server, 5432 secara default dan 5433 pada contoh modul ini.
usernameIdentitas pengguna database yang digunakan untuk autentikasi.
databaseNama database tujuan.
pool_sizeJumlah koneksi yang dipelihara dalam pool, defaultnya 10.
logPengaturan pencatatan query, false untuk meredam log debug.
queue_targetBatas tunggu pinjaman koneksi dalam milidetik sebelum permintaan mulai ditolak.

Konfigurasi khusus lingkungan development dituliskan pada config/dev.exs:

elixir Konfigurasi lingkungan development pada config/dev.exs
# tidak-dijalankan
import Config

config :perpustakaan, Perpustakaan.Repo,
  hostname: "127.0.0.1",
  port: 5433,
  database: "perpustakaan_dev",
  pool_size: 10

Pemahaman pool menentukan cara membaca perilaku Repo. pool_size mengatur berapa koneksi yang dibuka ke server, dan setiap operasi database meminjam salah satunya untuk sementara. Apabila seluruh koneksi sedang dipinjam, permintaan berikutnya menunggu di antrean selama queue_target milidetik sebelum mulai dibatalkan. Kunci log perlu diperhatikan pada skrip: secara default setiap query tercatat pada level debug beserta durasinya, sehingga keluaran program dipenuhi baris QUERY OK yang mencemari hasil cetak. Menyetel log: false meredam baris-baris tersebut.

Skrip utuh: koneksi pertama#

Skrip berikut merangkum seluruh materi di atas menjadi satu alur yang dapat dijalankan. Repo dikonfigurasi dan didefinisikan, database dibuat apabila belum ada melalui koneksi admin yang terpisah, kemudian Repo dijalankan dan beberapa query langsung dieksekusi:

elixir koneksi.exs
# koneksi.exs — membuka Repo dan menjalankan query pertama
Mix.install([{:ecto_sql, "~> 3.13"}, {:postgrex, "~> 0.21"}])
Logger.configure(level: :warning)

Application.put_env(:jalur, Repo,
  hostname: "127.0.0.1", port: 5433, username: "postgres",
  database: "jalur_repo", pool_size: 2, log: false, queue_target: 500)

defmodule Repo do
  use Ecto.Repo, otp_app: :jalur, adapter: Ecto.Adapters.Postgres
end

# membuat database bila belum ada, lewat koneksi admin yang terpisah
{:ok, admin} =
  Postgrex.start_link(
    hostname: "127.0.0.1", port: 5433, username: "postgres", database: "postgres"
  )

case Postgrex.query(admin, "CREATE DATABASE jalur_repo", []) do
  {:ok, _} -> :dibuat
  {:error, %{postgres: %{code: :duplicate_database}}} -> :sudah_ada
end

{:ok, _} = Repo.start_link()

# query langsung tanpa schema: SELECT 1
{:ok, hasil} = Repo.query("SELECT 1 AS satu")
IO.inspect(hasil.rows, label: "SELECT 1")

# query berparameter
{:ok, hitung} = Repo.query("SELECT $1::int + $2::int AS jumlah", [2, 3])
IO.inspect(hitung.rows, label: "SELECT $1 + $2")

# identitas koneksi yang sedang dipakai
{:ok, info} = Repo.query("SELECT current_database() AS db, current_user AS siapa")
IO.inspect(info.rows, label: "koneksi aktif")
text Keluaran
SELECT 1: [[1]]
SELECT $1 + $2: [[5]]
koneksi aktif: [["jalur_repo", "postgres"]]

Skrip ini dijalankan dengan perintah berikut dari folder topiknya:

bash
# menjalankan skrip koneksi
elixir koneksi.exs
text Keluaran
SELECT 1: [[1]]
SELECT $1 + $2: [[5]]
koneksi aktif: [["jalur_repo", "postgres"]]

Tiga hal pada skrip tersebut layak digarisbawahi. Pertama, database dibuat melalui koneksi admin ke database postgres karena perintah CREATE DATABASE tidak dapat dijalankan melalui Repo yang menunjuk database yang belum ada. Klausa penangkap duplicate_database membuat skrip tetap aman dijalankan ulang tanpa pesan apa pun. Kedua, Repo.query/2 mengembalikan {:ok, %Postgrex.Result{}} dengan kolom rows berupa list dari list, persis seperti [[1]] pada keluaran. Ketiga, query kedua menggunakan placeholder $1 dan $2 untuk nilai dari luar, sehingga nilai tidak pernah dirangkai ke dalam teks SQL. Penulisan SQL mentah seperti ini tidak diajarkan lebih jauh pada jalur Elixir karena keterampilan SQL dikuasai melalui jalur PostgreSQL.

Sandbox pengujian#

Sandbox pengujian membuat setiap tes berjalan di dalam transaksi database yang dibatalkan setelah tes selesai, sehingga data yang ditulis tes tidak pernah tersisa di database. Mekanisme ini disediakan oleh Ecto.Adapters.SQL.Sandbox dan dipasang dengan dua langkah, yaitu mengganti pool pada konfigurasi lingkungan tes dan menetapkan mode sandboxnya:

elixir Konfigurasi sandbox pada config/test.exs
# tidak-dijalankan
import Config

config :perpustakaan, Perpustakaan.Repo,
  hostname: "127.0.0.1",
  port: 5433,
  database: "perpustakaan_test",
  pool: Ecto.Adapters.SQL.Sandbox
elixir Menetapkan mode sandbox pada test/test_helper.exs
# tidak-dijalankan
Perpustakaan.Repo.start_link()
Ecto.Adapters.SQL.Sandbox.mode(Perpustakaan.Repo, :manual)
ExUnit.start()

Mode :manual berarti tidak ada tes yang memperoleh koneksi secara diam-diam. Setiap tes harus melakukan checkout, yaitu meminjam satu koneksi sandbox untuk dirinya, melalui Ecto.Adapters.SQL.Sandbox.start_owner!/2 pada callback setup, lalu melepaskannya dengan stop_owner/1 melalui on_exit. Pemisahan ini membuat tes dengan tag async: true dapat berjalan bersamaan tanpa saling mengganggu data, karena setiap pemilik koneksi bekerja di dalam transaksinya sendiri.

Kesalahan umum#

Database yang belum dibuat adalah kegagalan pertama yang paling sering muncul. Gejalanya menipu karena Repo.start_link() tetap mengembalikan {:ok, pid}, sedangkan error baru muncul pada query pertama berupa DBConnection.ConnectionError tentang koneksi yang tidak tersedia. Log pada saat itu menampilkan penyebab sebenarnya secara berulang, yaitu FATAL 3D000 (invalid_catalog_name) dengan pesan “database does not exist”. Perbaikannya adalah membuat database lebih dahulu melalui koneksi admin seperti yang dilakukan skrip koneksi.exs, bukan menaikkan pool_size yang tidak berhubungan dengan masalah.

Kesalahan kedua adalah melupakan log: false pada skrip atau peraga kecil. Tanpa kunci itu, setiap query menambahkan baris log debug QUERY OK berikut durasinya, sehingga keluaran yang seharusnya tiga baris menjadi puluhan baris dan sulit dibandingkan antareksekusi. Untuk program yang memang memerlukan pencatatan, redam level Logger ke :warning seperti yang dilakukan setiap skrip pada modul ini, karena baris log query berada di level debug.

Ringkasan#

  • Repo adalah process yang mengelola pool koneksi dan menjadi satu-satunya pintu masuk data. Konfigurasinya dibaca dari Application environment dengan otp_app dan nama modul Repo sebagai kuncinya.
  • Kunci konfigurasi utama mencakup hostname, port, username, database, pool_size, log, dan queue_target.
  • Repo.query!/2 menjalankan SQL mentah dengan nilai terikat melalui placeholder, dan hasilnya berupa list dari list pada rows.
  • Sandbox pengujian membungkus setiap tes dalam transaksi yang dibatalkan di akhir tes. Penggantian pool pada config/test.exs dan mode :manual pada test_helper.exs wajib dipasang berpasangan.

Referensi#