โ† Semua pembelajaran / Astro Nol โ†’ Portal Berita
Fase 2 ยท MySQL dengan Kysely

SELECT, JOIN & relasi bersarang

Bagian ini yang paling terasa akrab. Yang baru cuma satu: cara mengambil tag dan penulis sekaligus tanpa membangunkan N+1.

Sumber asli kysely.dev Resmi Rangkuman ~10 menit baca

Intisari

  • selectFrom().select().where().execute() โ€” bentuknya sama dengan CI3, tapi diperiksa compiler.
  • execute() memberi array, executeTakeFirst() memberi satu atau undefined, executeTakeFirstOrThrow() melempar.
  • leftJoin membuat kolom tabel kanan jadi nullable di tipe hasilnya โ€” otomatis dan benar.
  • jsonArrayFrom mengambil relasi bersarang dalam satu kueri; ini pengganti N+1 yang benar.
  • where berantai digabung dengan AND. Untuk OR pakai eb.or([โ€ฆ]) secara eksplisit.

Peta method

CodeIgniter 3Kysely
$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:

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.