SELECT, JOIN & relasi bersarang
Bagian ini yang paling terasa akrab. Yang baru cuma satu: cara mengambil tag dan penulis sekaligus tanpa membangunkan N+1.
Intisari
selectFrom().select().where().execute()โ bentuknya sama dengan CI3, tapi diperiksa compiler.execute()memberi array,executeTakeFirst()memberi satu atauundefined,executeTakeFirstOrThrow()melempar.leftJoinmembuat kolom tabel kanan jadi nullable di tipe hasilnya โ otomatis dan benar.jsonArrayFrommengambil relasi bersarang dalam satu kueri; ini pengganti N+1 yang benar.whereberantai digabung dengan AND. Untuk OR pakaieb.or([โฆ])secara eksplisit.
Peta method
| CodeIgniter 3 | Kysely |
|---|---|
$this->db->select('a, b') | .select(["a", "b"]) |
$this->db->from('artikel') | .selectFrom("artikel") |
->where('status', 'terbit') | .where("status", "=", "terbit") |
->where('hits >', 100) | .where("hits", ">", 100) |
->where_in('id', $ids) | .where("id", "in", ids) |
->like('judul', $q) | .where("judul", "like", `%${q}%`) |
->join('k', 'k.id = a.kategori_id') | .innerJoin("kategori as k", "k.id", "a.kategori_id") |
->join(โฆ, 'left') | .leftJoin(โฆ) |
->order_by('x', 'DESC') | .orderBy("x", "desc") |
->group_by('kategori_id') | .groupBy("kategori_id") |
->having('jml >', 5) | .having("jml", ">", 5) |
->limit(20, 40) | .limit(20).offset(40) |
->get()->result() | .execute() |
->get()->row() | .executeTakeFirst() |
->count_all_results() | .select(({ fn }) => fn.countAll().as("n")) |
Tiga cara mengeksekusi
const semua = await q.execute(); // Artikel[]
const satu = await q.executeTakeFirst(); // Artikel | undefined
const wajib = await q.executeTakeFirstOrThrow(); // Artikel, atau melempar
Pilih yang ketiga hanya kalau ketiadaan baris benar-benar bug โ misalnya mengambil pengaturan situs yang
pasti ada. Untuk artikel berdasarkan slug, gunakan yang kedua dan tangani undefined dengan
404; melempar akan menghasilkan 500, dan 500 untuk artikel yang tidak ada adalah sinyal yang salah bagi
mesin pencari maupun bagi pemantauanmu.
Kueri daftar artikel yang lengkap
// src/lib/artikel.ts
export async function ambilArtikelTerbaru(kategoriSlug: string, batas = 20) {
let q = db
.selectFrom("artikel as a")
.innerJoin("member as p", "p.id", "a.penulis_id")
.leftJoin("kategori as k", "k.id", "a.kategori_id")
.select([
"a.id", "a.judul", "a.slug", "a.ringkasan",
"a.premium", "a.terbit_pada",
"p.nama as penulis",
"k.nama as kategori",
"k.slug as kategoriSlug",
])
.where("a.status", "=", "terbit")
.where("a.terbit_pada", "<=", new Date())
.orderBy("a.terbit_pada", "desc")
.limit(batas);
if (kategoriSlug !== "semua") {
q = q.where("k.slug", "=", kategoriSlug);
}
return q.execute();
}
Dua hal yang layak diperhatikan:
- Kueri bisa dibangun bertahap.
q = q.where(โฆ)mengembalikan kueri baru; ia tidak mengubah yang lama. Ini pengganti langsung untukifdi sekitar$this->db->where()di CI3, tapi tanpa efek samping tersembunyi pada objek query builder global yang sama. terbit_pada <= sekarangbukan detail kecil. Portal berita menjadwalkan artikel. Tanpa syarat ini, artikel yang dijadwalkan terbit besok pagi akan tampil sekarang โ bocor sebelum embargo.
Tipe hasilnya sudah benar tanpa kamu menuliskannya: penulis bertipe string
(inner join), kategori bertipe string | null (left join). Compiler akan memaksamu
menangani kemungkinan artikel tanpa kategori.
Kondisi OR dan bersarang
const hasil = await db
.selectFrom("artikel")
.selectAll()
.where("status", "=", "terbit")
.where((eb) =>
eb.or([
eb("judul", "like", `%${kata}%`),
eb("ringkasan", "like", `%${kata}%`),
]),
)
.execute();
where berantai selalu AND. OR harus eksplisit โ dan itu disengaja. Di CI3,
or_where() yang bercampur dengan where() menghasilkan pengelompokan yang tidak
selalu seperti yang kamu kira, dan bug otorisasi paling berbahaya lahir dari sana:
WHERE pemilik = 5 AND status = 'draf' OR status = 'terbit' memberikan seluruh artikel terbit
kepada siapa pun.
Tapi jangan pakai LIKE '%kata%' untuk pencarian. Wildcard di depan membuat MySQL
tidak bisa memakai index sama sekali โ ia memindai seluruh tabel. Di 200.000 artikel dan trafik
portalmu, satu pencarian bisa menahan koneksi selama detik-detik dan menyeret seluruh situs. Fase 9
membahas jalan keluarnya.
Relasi bersarang tanpa N+1
Masalah klasik: menampilkan 20 artikel, masing-masing dengan daftar tag.
// BURUK: 1 + 20 kueri
const artikel = await ambilArtikelTerbaru("semua", 20);
for (const a of artikel) {
a.tag = await ambilTag(a.id); // satu kueri per artikel
}
// BAIK: satu kueri
import { jsonArrayFrom } from "kysely/helpers/mysql";
const artikel = await db
.selectFrom("artikel as a")
.select((eb) => [
"a.id", "a.judul", "a.slug",
jsonArrayFrom(
eb.selectFrom("artikel_tag as at")
.innerJoin("tag as t", "t.id", "at.tag_id")
.select(["t.id", "t.nama", "t.slug"])
.whereRef("at.artikel_id", "=", "a.id"),
).as("tag"),
])
.where("a.status", "=", "terbit")
.orderBy("a.terbit_pada", "desc")
.limit(20)
.execute();
artikel[0].tag[0].nama; // bertipe string
jsonArrayFrom memakai JSON_ARRAYAGG milik MySQL. Hasilnya sudah berupa array
objek bertipe โ Kysely yang mengurus parsing JSON-nya. Impor helper-nya dari
kysely/helpers/mysql, bukan dari kysely/helpers/postgres; keduanya ada dan hanya
berbeda di implementasinya.
Ada batasnya. JSON_ARRAYAGG tunduk pada group_concat_max_len dan
memotong hasilnya diam-diam saat melewatinya. Untuk daftar tag per artikel โ beberapa entri โ aman.
Untuk mengambil semua komentar di satu artikel populer, jangan. Ambil terpisah dengan
where("artikel_id", "in", ids) lalu kelompokkan di JavaScript: dua kueri, bukan dua puluh
satu.
Menghitung total untuk paginasi
const { total } = await db
.selectFrom("artikel")
.select((eb) => eb.fn.countAll<number>().as("total"))
.where("status", "=", "terbit")
.executeTakeFirstOrThrow();
COUNT(*) di tabel besar itu mahal. InnoDB harus memindai index untuk menghitung,
dan di 200.000 baris dengan filter itu bisa puluhan milidetik โ dikalikan tiap kunjungan ke halaman
daftar. Untuk paginasi portal berita, simpan hitungan perkiraan di tabel terpisah yang diperbarui
berkala, atau tampilkan paginasi tanpa total sama sekali ("Berikutnya โ" saja). Pembaca beritamu tidak
pernah butuh tahu bahwa ada 9.412 halaman.
Melihat SQL yang dihasilkan
const { sql, parameters } = q.compile();
console.log(sql, parameters);
Lakukan ini setiap kali kamu ragu. Salah satu keuntungan terbesar Kysely dibanding ORM adalah SQL-nya
selalu bisa kamu lihat dan tempelkan ke EXPLAIN โ tanpa menjalankan kuerinya.
Latihan: tulis ulang kueri daftar berita CI3-mu yang sebenarnya di Kysely, lengkap dengan join
penulis dan kategori. Panggil .compile() dan bandingkan SQL yang keluar dengan hasil
$this->db->last_query() di CI3 โ keduanya harus setara. Lalu tambahkan daftar tag dengan
jsonArrayFrom dan jalankan EXPLAIN pada hasilnya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.