Query pada Ecto bukan teks SQL melainkan data: sebuah query dibangun sebagai nilai %Ecto.Query{} yang dapat disimpan dalam variabel, dilewatkan melalui pipeline, dan diperkaya bertahap sebelum dikirim ke database. Topik ini membahas pembentukan query dengan from/2, penyusunan bertahap dengan where, select, dan order_by, pengikatan nilai dari luar melalui operator pin, serta join antartabel pada katalog perpustakaan. Penulisan SQL mentah sendiri tidak menjadi fokus jalur Elixir karena keterampilan SQL dikuasai melalui jalur PostgreSQL.
Query sebagai data yang disusun#
from/2 membentuk query sebagai struct, sehingga query adalah nilai biasa yang dapat disimpan, dibandingkan, dan disusun tanpa merusak nilai asalnya. Ekspresi from(b in Buku, ...) menetapkan b sebagai pengikat (binding) yang mewakili tabel buku, dan setiap fungsi penyusun menerima query lama lalu mengembalikan query baru dengan kriteria tambahan. Query asal tetap utuh karena seluruh data Elixir bersifat imutabel, sehingga satu query dasar boleh menjadi induk bagi beberapa query turunan sekaligus.
Modul Pencarian pada skrip di bawah memuat query dasar beserta dua turunannya. Fungsi dasar_judul/0 menyusun dasar/0 dengan select dan order_by, sedangkan dasar_tersaring/0 menyusun dasar_judul/0 sekali lagi dengan where:
# tidak-dijalankan
defmodule Pencarian do
import Ecto.Query
def dasar do
from(b in Buku, where: b.tahun >= 2019)
end
def dasar_judul do
dasar()
|> select([b], b.judul)
|> order_by([b], b.judul)
end
def dasar_tersaring do
dasar_judul()
|> where([b], b.tahun <= 2020)
end
endimport Ecto.Query pada modul tersebut membuat from, where, select, dan order_by dapat dipanggil tanpa awalan modul, sebagaimana kebiasaan modul context pada proyek Phoenix. Komposisi di atas memisahkan tanggung jawab: query dasar hanya menyatakan syarat tahun, sedangkan turunannya menambahkan bentuk keluaran dan pengurutan masing-masing. Perubahan syarat dasar otomatis berlaku pada seluruh turunannya, dan perilaku ini membedakan komposisi query dari penyalinan teks SQL.
Semantik tiap fungsi penyusun juga perlu dipahami agar komposisi terbaca dengan benar. Pemanggilan where lebih dari satu kali menggabungkan seluruh syarat dengan AND, sedangkan select yang dipanggil belakangan menggantikan bentuk keluaran sebelumnya tanpa menyimpan bentuk lamanya. Dengan demikian, urutan penyusunan tidak memengaruhi syarat penyaringan, tetapi menentukan bentuk akhir keluaran melalui satu select terakhir. Kebiasaan membaca query panjang pun menjadi sederhana: cari seluruh syarat where pada urutan komposisinya, lalu perhatikan satu select paling akhir untuk mengetahui bentuk data yang kembali.
Menyaring, memilih, dan mengurutkan#
where/3 menambahkan syarat baris, select/3 menentukan bentuk hasil, dan order_by/3 menetapkan urutannya, sedangkan ketiganya menerima pengikat yang sama seperti pada from/2. Tanpa select, query mengembalikan struct lengkap dari schema, sedangkan select: b.judul mengembalikan nilai mentah dan select: {b.judul, b.tahun} mengembalikan tuple berurutan. order_by: [asc: b.tahun, asc: b.judul] mengurutkan menurut beberapa kolom sekaligus, dan pengurutan eksplisit semacam ini wajib pada data yang akan dibandingkan antareksekusi karena urutan kembalian database tanpa order_by tidak dijamin.
Nilai dari luar query tidak pernah dirangkai sebagai teks. Operator pin ^ mengikat nilai variabel Elixir ke dalam query, sehingga nilai tersebut dikirim sebagai parameter terpisah dan tidak pernah menjadi bagian dari teks SQL:
# tidak-dijalankan
pengarang = "Ali Rahman"
from(b in Buku, where: b.pengarang == ^pengarang, select: b.judul)Pengikatan ini melindungi dua hal sekaligus. Pertama, nilai yang mengandung tanda kutip atau karakter apa pun tidak dapat mengubah maksud query karena database menerimanya sebagai data, bukan sebagai perintah. Kedua, query dengan pengikat berbeda tetap berupa satu bentuk pernyataan yang sama di sisi server, sehingga pemanggilan berulang dengan nilai berbeda tidak memaksa penyusunan rencana eksekusi baru.
Skrip utuh: pencarian katalog#
Skrip berikut membangun katalog lima buku beserta tiga catatan peminjamannya, lalu menjawab lima pertanyaan berbeda dengan query yang disusun dari atas ke bawah. Migrasi membuat tabel buku dan peminjaman tanpa deklarasi asosiasi, karena penggabungan pada topik ini dilakukan dengan join eksplisit:
# pencarian.exs — query yang dapat disusun di atas data 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_query", 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_query", []) 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
end
end
defmodule Peminjaman do
use Ecto.Schema
schema "peminjaman" do
field :anggota, :string
field :status, :string
field :buku_id, :integer
end
end
defmodule BuatBuku do
use Ecto.Migration
def up do
create table(:buku) do
add :judul, :string
add :pengarang, :string
add :tahun, :integer
end
end
def down, do: drop table(:buku)
end
defmodule BuatPeminjaman do
use Ecto.Migration
def up do
create table(:peminjaman) do
add :anggota, :string
add :status, :string
add :buku_id, :integer
end
end
def down, do: drop table(:peminjaman)
end
Ecto.Migrator.run(Repo, [{20261106000001, BuatBuku}, {20261106000002, BuatPeminjaman}], :up, all: true)
Repo.insert_all(Buku, [
%{judul: "Pengantar Logika", pengarang: "Citra Kusuma", tahun: 2018},
%{judul: "Sejarah Maritim Nusantara", pengarang: "Ali Rahman", tahun: 2020},
%{judul: "Panduan Menanam Cabai", pengarang: "Dewi Anggraini", tahun: 2019},
%{judul: "Kamus Indonesia-Inggris", pengarang: "Eka Prasetyo", tahun: 2021},
%{judul: "Dasar Astronomi", pengarang: "Bambang Susilo", tahun: 2019}
])
id_logika = Repo.get_by!(Buku, judul: "Pengantar Logika").id
id_cabai = Repo.get_by!(Buku, judul: "Panduan Menanam Cabai").id
id_kamus = Repo.get_by!(Buku, judul: "Kamus Indonesia-Inggris").id
Repo.insert_all(Peminjaman, [
%{buku_id: id_logika, anggota: "Budi Santoso", status: "dipinjam"},
%{buku_id: id_cabai, anggota: "Rina Melati", status: "dipinjam"},
%{buku_id: id_kamus, anggota: "Fajar Nugroho", status: "kembali"}
])
defmodule Pencarian do
import Ecto.Query
def semua_buku do
from(b in Buku, order_by: [asc: b.tahun, asc: b.judul], select: {b.judul, b.tahun})
end
def dasar do
from(b in Buku, where: b.tahun >= 2019)
end
# query dasar disusun bertahap lewat pipeline
def dasar_judul do
dasar()
|> select([b], b.judul)
|> order_by([b], b.judul)
end
def dasar_tersaring do
dasar_judul()
|> where([b], b.tahun <= 2020)
end
def karya(nama) do
from(b in Buku, where: b.pengarang == ^nama, select: b.judul)
end
def dipinjam do
from(p in Peminjaman,
join: b in Buku,
on: b.id == p.buku_id,
where: p.status == "dipinjam",
select: {b.judul, p.anggota},
order_by: b.judul
)
end
end
IO.puts("semua buku (urut tahun, lalu judul):")
Repo.all(Pencarian.semua_buku())
|> Enum.each(fn {judul, tahun} -> IO.puts(" #{tahun} #{judul}") end)
IO.puts("query dasar (tahun >= 2019):")
Repo.all(Pencarian.dasar_judul())
|> Enum.each(&IO.puts(" " <> &1))
IO.puts("query tersusun (tahun <= 2020):")
Repo.all(Pencarian.dasar_tersaring())
|> Enum.each(&IO.puts(" " <> &1))
pengarang = "Ali Rahman"
IO.puts("karya #{pengarang}:")
Repo.all(Pencarian.karya(pengarang))
|> Enum.each(&IO.puts(" " <> &1))
IO.puts("buku yang sedang dipinjam:")
Repo.all(Pencarian.dipinjam())
|> Enum.each(fn {judul, anggota} -> IO.puts(" #{judul} dipinjam oleh #{anggota}") end)semua buku (urut tahun, lalu judul):
2018 Pengantar Logika
2019 Dasar Astronomi
2019 Panduan Menanam Cabai
2020 Sejarah Maritim Nusantara
2021 Kamus Indonesia-Inggris
query dasar (tahun >= 2019):
Dasar Astronomi
Kamus Indonesia-Inggris
Panduan Menanam Cabai
Sejarah Maritim Nusantara
query tersusun (tahun <= 2020):
Dasar Astronomi
Panduan Menanam Cabai
Sejarah Maritim Nusantara
karya Ali Rahman:
Sejarah Maritim Nusantara
buku yang sedang dipinjam:
Panduan Menanam Cabai dipinjam oleh Rina Melati
Pengantar Logika dipinjam oleh Budi SantosoSkrip ini dijalankan dengan perintah berikut dari folder topiknya:
# menjalankan skrip pencarian
elixir pencarian.exssemua buku (urut tahun, lalu judul):
2018 Pengantar Logika
2019 Dasar Astronomi
2019 Panduan Menanam Cabai
2020 Sejarah Maritim Nusantara
2021 Kamus Indonesia-Inggris
query dasar (tahun >= 2019):
Dasar Astronomi
Kamus Indonesia-Inggris
Panduan Menanam Cabai
Sejarah Maritim Nusantara
query tersusun (tahun <= 2020):
Dasar Astronomi
Panduan Menanam Cabai
Sejarah Maritim Nusantara
karya Ali Rahman:
Sejarah Maritim Nusantara
buku yang sedang dipinjam:
Panduan Menanam Cabai dipinjam oleh Rina Melati
Pengantar Logika dipinjam oleh Budi SantosoBagian query tersusun memperlihatkan cara kerja komposisi: query dasarnya memuat empat buku terbitan 2019 ke atas, dan penambahan where: b.tahun <= 2020 mempersempit hasilnya menjadi tiga tanpa menulis ulang syarat pertama. Bagian terakhir menunjukkan join/5 yang menggabungkan peminjaman dengan buku melalui syarat on: b.id == p.buku_id, disaring pada status peminjaman yang aktif, lalu dikembalikan sebagai tuple judul dan nama anggota. Baris peminjaman dengan status "kembali" tidak muncul karena syarat where-nya, dan urutan hasilnya mengikuti order_by pada judul.
Kesalahan umum#
Lupa menambahkan import Ecto.Query adalah kegagalan kompilasi yang paling awal dialami: ekspresi from/2 tidak dikenal dan compiler melaporkan fungsi yang tidak terdefinisi. Gejala ini muncul sebelum program berjalan, sehingga perbaikannya cepat, yaitu menambahkan import Ecto.Query pada modul yang membangun query. Modul yang hanya mengeksekusi query melalui Repo.all/1 tidak membutuhkan import karena eksekusi query adalah pemanggilan fungsi biasa.
Interpolasi teks ke dalam query adalah kesalahan yang jauh lebih berbahaya. Menulis where: b.pengarang == "#{nama}" memasukkan nilai ke dalam teks SQL, sehingga nilai yang mengandung tanda kutip dapat mengubah maksud pernyataan, dan bentuk query berubah untuk setiap nilai yang berbeda. Operator pin ^ menghilangkan kedua masalah itu sekaligus karena nilai dikirim sebagai parameter, dan kebiasaan yang benar adalah mengikat setiap nilai dari luar melalui pin tanpa kecuali.
Ringkasan#
- Query Ecto adalah struct
%Ecto.Query{}yang dibentukfrom/2dan disusun bertahap olehwhere/3,select/3, danorder_by/3tanpa mengubah query asalnya. - Operator pin
^mengikat nilai Elixir sebagai parameter query, sehingga nilai tidak pernah dirangkai ke dalam teks SQL. join/5menggabungkan tabel melalui syaraton, dan hasilnya dibentuk olehselectbaik sebagai struct, nilai tunggal, maupun tuple.- Pengurutan pada
order_bywajib dinyatakan secara eksplisit apabila urutan hasil perlu dipercaya.
Referensi#
- Ecto.Query — Hexdocs Ecto — dokumentasi resmi DSL query beserta seluruh fungsinya.
- from/2 — Hexdocs Ecto — bentuk dasar query dan opsi yang diterimanya.
- where/3 — Hexdocs Ecto — penyaringan baris dan pengikatan nilai dengan pin.
- join/5 — Hexdocs Ecto — penggabungan tabel dan syarat on-nya.