Belajar GraphQL - Solving N+1 Problem dengan DataLoader
Episode 9 of 51

Belajar GraphQL - Solving N+1 Problem dengan DataLoader

Episode 9 menaklukkan N+1 problem, masalah performa klasik GraphQL: memahami penyebab N+1 saat resolver chain memicu query berulang, konsep batching dan caching DataLoader, implementasi batch loader, integrasi loader ke context per request, hingga pengukuran perbaikan performa.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

Episode 6 sempat menyinggung bahaya resolver chain. Kini saatnya membedah bahaya tersebut secara mendalam: N+1 problem, musuh performa paling terkenal di GraphQL. Episode 9 menjelaskan mengapa GraphQL rentan terhadapnya dan bagaimana DataLoader menjadi solusi standarnya.

Kita akan memahami mekanisme N+1, konsep batching dan caching DataLoader, cara mengimplementasikannya, integrasi loader ke dalam context yang per-request, hingga teknik mengukur perbaikan performa setelah loader dipasang.

N+1 Query Problem

Bagaimana N+1 Terjadi

Perhatikan query berikut:

Query yang memicu N+1
query DaftarPost {
  posts {
    id
    title
    author {
      name
    }
  }
}

Jika ada 100 post, resolver posts memicu 1 query, lalu resolver Post.author dipanggil 100 kali — satu kali per post — dan masing-masing memicu 1 query ke tabel users. Total 1 + 100 = 101 query untuk satu request. Itulah asal nama N+1.

Penyebabnya: GraphQL memanggil resolver per-field-per-item, dan default resolver mengembalikan data dari parent secara independen. Pola ini membuat query terlihat efisien di client, tapi boros di database.

Impact terhadap Performa

Dampaknya nyata: latensi request membengkak, database kehabisan koneksi, dan biaya infrastruktur naik. Di production dengan ribuan user, N+1 adalah masalah nomor satu yang ditemukan saat load testing. Identifikasi awal bisa dilakukan dengan melihat log query: jika muncul pola query identik berulang kali dengan id berbeda, hampir pasti N+1.

DataLoader Fundamentals

Konsep Batching dan Caching

DataLoader adalah library yang dirancang untuk masalah ini dengan dua mekanisme:

  • Batching: semua panggilan load dalam satu tick event loop digabung menjadi satu pemanggilan batch function.
  • Caching: hasil per key di-cache selama siklus hidup loader, sehingga key yang sama tidak di-fetch dua kali.
Install DataLoader
npm install dataloader

Jalankan npm install dataloader untuk menambahkan library ini ke project.

Membuat Batch Loader

JSBatch loader dasar
import DataLoader from "dataloader";
 
const batchUsers = async (ids) => {
  const users = await db.users.findMany({ where: { id: { in: ids } } });
  const byId = new Map(users.map((u) => [u.id, u]));
  return ids.map((id) => byId.get(id));
};
 
const userLoader = new DataLoader(batchUsers);

Perhatikan dua detail penting. Pertama, fungsi batch menerima array ids dan harus mengembalikan hasil dengan urutan yang sama seperti input. Kedua, jika sebuah id tidak ditemukan, kembalikan null untuk posisinya agar urutan tidak bergeser.

Implementasi DataLoader

Memakai Loader di Resolver

Loader mengubah resolver Post.author dari query per-item menjadi satu query batch:

JSResolver dengan loader
const resolvers = {
  Query: {
    posts: async (_, __, ctx) => ctx.db.posts.findMany(),
  },
  Post: {
    author: (post, _, ctx) => ctx.loaders.userById.load(post.authorId),
  },
};

Sekarang 100 post hanya memicu 1 query users. Batching bekerja karena semua panggilan load dari resolver-resolver yang berjalan paralel terkumpul dalam satu tick, lalu digabung oleh DataLoader.

Caching dan Clear Cache

Cache DataLoader bersifat per-instance. Karena itu loader harus dibuat per request, bukan global — jika global, data yang sudah berubah (misalnya setelah mutation) akan tertutup cache lama. Untuk data yang baru saja dimutasi, panggil loader.clear(key) agar di-fetch ulang, atau bangun loader baru di setiap request.

Error Handling dalam Loader

Jika batch function melempar error, seluruh load dalam batch tersebut gagal. Untuk penanganan per-item yang lebih halus, kembalikan hasil per item dan throw error secara selektif. Pola umum: kembalikan null untuk item yang tidak ditemukan, dan baru throw bila seluruh batch bermasalah.

Integrasi dengan Context

Request-Scoped Loaders

Tempat yang tepat untuk loader adalah di dalam fungsi context yang dipanggil per request:

JSLoader per request di context
const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => ({
    user: await authenticateUser(req.headers.authorization),
    db,
    loaders: {
      userById: new DataLoader(batchUsers),
      postByAuthorId: new DataLoader(batchPostsByAuthor),
      commentByPostId: new DataLoader(batchCommentsByPost),
    },
  }),
});

Dengan pola ini, setiap request mendapat instance loader baru: batching berlaku per request, dan cache tidak bocor antar request. Semua resolver memakai loader yang sama melalui ctx.loaders, sehingga tidak ada duplikasi definisi.

Monitoring dan Debugging

Mengukur Perbaikan

Ukur jumlah query database sebelum dan sesudah memakai DataLoader. Alat sederhananya: hitung query di log, atau gunakan plugin Prisma untuk logging. Setelah loader terpasang, pola query identik berulang harus menghilang dan digantikan satu query batch dengan IN.

Untuk debugging batch call, tambahkan log di dalam batch function — kalian akan melihat ids terkumpul dalam satu array, bukan dipanggil satu-satu. Ini bukti batching bekerja. DataLoader juga bisa diberi opsi maxBatchSize bila satu request bisa menghasilkan batch raksasa.

Penutup

Inti yang harus dibawa pulang:

  • N+1 terjadi saat resolver chain memicu query berulang: 1 query awal ditambah N query tambahan.
  • DataLoader menggabungkan banyak load menjadi satu batch function call.
  • Batch function harus mengembalikan hasil sejajar dengan urutan id input.
  • Cache DataLoader bersifat per-instance; buat loader baru per request.
  • Pakai loader.clear(key) setelah mutation agar data segar.
  • Ukur jumlah query sebelum dan sesudah untuk membuktikan perbaikan.

Di episode 10 selanjutnya kalian akan mempelajari validasi dan sanitasi input — validasi otomatis di level schema, integrasi Zod untuk validasi resolvers, sanitasi untuk mencegah XSS dan SQL injection, hingga desain error response yang ramah client. Data yang masuk ke server kalian tidak akan lagi lepas kontrol!