Schema dan migration adalah dua sisi dari satu pekerjaan yang sama, yaitu menetapkan bentuk data: schema menetapkan bentuk pada sisi Elixir sebagai struct, sedangkan migration menetapkan bentuk pada sisi database sebagai tabel. Topik ini membahas keduanya pada tabel buku perpustakaan, mulai dari definisi schema dengan use Ecto.Schema, migrasi dengan use Ecto.Migration dan Ecto.Migrator.run/4, sampai penyimpanan dan pembacaan baris pertama. Tabel buku ini kemudian digunakan kembali pada pembahasan changeset, query, dan asosiasi.
Schema sebagai peta tabel#
Schema adalah modul Elixir yang memetakan satu tabel database ke satu struct, sehingga setiap baris tabel menjadi nilai struct yang dapat dipola dan dialirkan seperti data Elixir lain. Definisinya diawali use Ecto.Schema, diikuti blok schema/1 yang menyebut nama tabelnya, lalu daftar field yang menyatakan kolom beserta tipenya:
# tidak-dijalankan
defmodule Buku do
use Ecto.Schema
schema "buku" do
field :judul, :string
field :pengarang, :string
field :tahun, :integer
timestamps()
end
endTiga mekanisme pada definisi tersebut bekerja secara berbeda. schema "buku" menghubungkan struct dengan tabel bernama buku, dan nama modulnya bebas sehingga modul Buku boleh memetakan tabel buku tanpa aturan penamaan yang ketat. Setiap field menambahkan kunci baru pada struct beserta tipenya, dan Ecto menggunakan tipe ini untuk mengubah nilai Elixir menjadi nilai database dan sebaliknya. Adapun timestamps() menambahkan dua kolom sekaligus, yaitu inserted_at dan updated_at, yang diisi Ecto secara otomatis setiap kali baris disimpan atau diubah.
Schema juga tetap dapat digunakan tanpa database sama sekali. struct(Buku, judul: "Pengantar Logika") membangun struct buku di memori tanpa menyentuh koneksi, sehingga fungsi yang menerima %Buku{} dapat diuji terpisah dari lapisan persistensinya. Pemisahan ini membuat schema bukan sekadar daftar kolom, melainkan kontrak bentuk data yang dipegang seluruh aplikasi.
Migration dan catatan versinya#
Migration adalah modul yang mengubah bentuk database secara bertahap, yaitu menambah tabel, kolom, atau indeks, dan setiap langkahnya dicatat sebagai satu versi bernomor. Definisinya diawali use Ecto.Migration, dan fungsi up/0 memuat perubahan yang ingin diterapkan sedangkan down/0 memuat kebalikannya:
# tidak-dijalankan
defmodule BuatBuku do
use Ecto.Migration
def up do
create table(:buku) do
add :judul, :string
add :pengarang, :string
add :tahun, :integer
timestamps()
end
end
def down, do: drop table(:buku)
endcreate table(:buku) menambahkan kolom id bertipe bigserial secara otomatis sebagai kunci utama, dan add menyatakan kolom lainnya dengan tipe yang sama seperti pada schema. Dua modul di atas baru berupa definisi, sehingga penerapannya dilakukan oleh Ecto.Migrator.run/4 yang menerima daftar pasangan versi dan modul migrasinya:
# tidak-dijalankan
Ecto.Migrator.run(Repo, [{20261106000001, BuatBuku}], :up, all: true)Nomor versi 20261106000001 berperan dalam pencatatan migrasi. Setelah migrasi lulus, Ecto mencatat nomor itu ke tabel schema_migrations, sehingga migrasi yang sudah diterapkan tidak dijalankan ulang pada eksekusi berikutnya. Perintah mix ecto.migrate pada proyek sungguhan menjalankan mekanisme yang sama dengan membaca berkas migrasi dari folder priv/repo/migrations, dan nomor versinya lazim ditulis sebagai tanggal pengerjaan migrasi.
Skrip utuh: katalog buku#
Skrip berikut menggabungkan schema, migrasi, dan data pertama menjadi satu alur yang dapat dijalankan berulang. Database direset terlebih dahulu agar selalu dimulai dari kondisi yang sama, migrasi diterapkan, tiga buku disimpan, lalu isi katalog dibaca kembali:
# katalog.exs — schema Buku, migrasi, dan katalog perpustakaan
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_schema", 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_schema", []) do
{:ok, _} -> :dibuat
{:error, %{postgres: %{code: :duplicate_database}}} -> :sudah_ada
end
{:ok, _} = Repo.start_link()
Repo.query!("DROP SCHEMA public CASCADE")
Repo.query!("CREATE SCHEMA public")
defmodule Buku do
use Ecto.Schema
schema "buku" do
field :judul, :string
field :pengarang, :string
field :tahun, :integer
timestamps()
end
end
defmodule BuatBuku do
use Ecto.Migration
def up do
create table(:buku) do
add :judul, :string
add :pengarang, :string
add :tahun, :integer
timestamps()
end
end
def down, do: drop table(:buku)
end
Ecto.Migrator.run(Repo, [{20261106000001, BuatBuku}], :up, all: true)
Repo.insert!(struct(Buku, judul: "Pengantar Logika", pengarang: "Citra Kusuma", tahun: 2018))
Repo.insert!(struct(Buku, judul: "Sejarah Maritim Nusantara", pengarang: "Ali Rahman", tahun: 2020))
Repo.insert!(struct(Buku, judul: "Panduan Menanam Cabai", pengarang: "Dewi Anggraini", tahun: 2019))
IO.puts("katalog buku:")
Repo.all(Buku)
|> Enum.sort_by(& &1.judul)
|> Enum.each(fn b -> IO.puts(" #{b.judul} (#{b.tahun}) oleh #{b.pengarang}") end)
[[versi]] = Repo.query!("SELECT version FROM schema_migrations").rows
IO.puts("versi migrasi tercatat: #{versi}")
IO.puts("jumlah baris: #{Repo.aggregate(Buku, :count, :id)}")katalog buku:
Panduan Menanam Cabai (2019) oleh Dewi Anggraini
Pengantar Logika (2018) oleh Citra Kusuma
Sejarah Maritim Nusantara (2020) oleh Ali Rahman
versi migrasi tercatat: 20261106000001
jumlah baris: 3Skrip ini dijalankan dengan perintah berikut dari folder topiknya:
# menjalankan skrip katalog
elixir katalog.exskatalog buku:
Panduan Menanam Cabai (2019) oleh Dewi Anggraini
Pengantar Logika (2018) oleh Citra Kusuma
Sejarah Maritim Nusantara (2020) oleh Ali Rahman
versi migrasi tercatat: 20261106000001
jumlah baris: 3Tiga baris pertama keluaran berasal dari Repo.all/1 yang mengembalikan list struct Buku, dan urutannya dijaga oleh Enum.sort_by/2 karena urutan kembalian database tanpa order_by tidak dijamin. Baris keempat menunjukkan bahwa Ecto.Migrator.run/4 telah menulis versi migrasinya ke schema_migrations, sehingga pemanggilan ulang skrip tidak menjalankan up/0 dua kali pada migrasi yang sama. Repo.insert!/1 yang menyimpan tiap buku mengembalikan struct lengkap dengan id dan kedua timestamp, dan kedua nilai waktu itu sengaja tidak dicetak karena bukan bagian dari data yang dipelajari.
Kesalahan umum#
Ketidaksesuaian antara schema dan migration adalah sumber error yang paling membingungkan karena gejalanya muncul jauh dari penyebabnya. Field yang ada di schema tetapi tidak ada di tabel memunculkan error undefined column saat query dijalankan. Sebaliknya, kolom yang ada di tabel tetapi lupa dinyatakan di schema tidak menimbulkan error sama sekali, hanya saja datanya tidak pernah terlihat dari Elixir. Kebiasaan yang mencegah keduanya adalah menuliskan schema dan migration berdampingan pada satu perubahan, sehingga setiap add pada migrasi memiliki pasangan field pada schema.
Kesalahan kedua adalah menjalankan isi migrasi secara manual di luar Ecto.Migrator.run/4. Pemanggilan langsung fungsi migrasi tidak mencatat versi apa pun ke schema_migrations, sehingga eksekusi berikutnya menjalankan up/0 lagi dan berakhir dengan error tabel yang sudah ada. Catatan versi adalah seluruh nilai dari mekanisme migrasi, sehingga migrasi selalu dieksekusi melalui migratornya, baik melalui Ecto.Migrator.run/4 pada skrip maupun melalui mix ecto.migrate pada proyek.
Ringkasan#
- Schema memetakan satu tabel ke satu struct melalui
use Ecto.Schema, blokschema/1, danfield, dengantimestamps()menambahkaninserted_atdanupdated_atyang diisi otomatis. - Migration mengubah bentuk database dalam fungsi
up/0dandown/0, dan penerapannya melaluiEcto.Migrator.run/4dicatat sebagai nomor versi pada tabelschema_migrations. Repo.all/1mengembalikan list struct dari hasil query, sedangkanRepo.insert!/1menyimpan satu struct dan mengembalikan versi lengkapnya.- Ketidaksesuaian schema dan migration sebaiknya dicegah dengan menulis keduanya berdampingan pada setiap perubahan bentuk data.
Referensi#
- Ecto.Schema — Hexdocs Ecto — dokumentasi resmi schema, field, dan timestamps.
- Ecto.Migration — Hexdocs Ecto SQL — dokumentasi perintah migrasi seperti create table dan add.
- Ecto.Migrator — Hexdocs Ecto SQL — mekanisme eksekusi migrasi dan catatan versinya.
- Ecto.Schema:
timestamps/1— Hexdocs Ecto — rincian kolom waktu yang ditambahkan timestamps.