tutorial

119 Optimasi Query Resolver dengan dataloader

119 Optimasi Query Resolver dengan dataloader

Ketika membangun API dengan GraphQL, salah satu tantangan terbesar adalah menghindari masalah N+1 query yang sering tersembunyi di balik layer resolver. Gagal mengoptimasi resolver akan mengakibatkan database bekerja lebih keras dari seharusnya, menurunkan performa aplikasi, hingga membengkaknya tagihan cloud database. Untungnya kita punya satu alat andalan yang bisa diandalkan: dataloader. Pada artikel ke-119 saya kali ini, saya akan membedah bagaimana cara dataloader bekerja, cara implementasinya, dan bagaimana dia mampu memberikan optimasi signifikan pada query resolver. Kita juga akan lakukan simulasi kasus nyata, sebelum dan sesudah menggunakan DataLoader.


Memahami Masalah: N+1 Query

Bayangkan sebuah API yang memiliki skema GraphQL seperti berikut:

type Post {
  id: ID!
  title: String!
  author: User!
}

type User {
  id: ID!
  name: String!
}

type Query {
  posts: [Post!]!
}

Ketika client meminta seluruh posts beserta data author dari setiap post:

query {
  posts {
    id
    title
    author {
      id
      name
    }
  }
}

Resolver naïf yang biasa ditulis adalah seperti ini (pseudo-JS):

const resolvers = {
  Query: {
    posts: () => PostModel.findAll(),
  },
  Post: {
    author: (post) => UserModel.findById(post.authorId),
  }
}

Terdengar biasa. Tapi, jika ada 10 post, maka akan terjadi:

  • 1 query untuk posts
  • 10 query untuk user (get author dari tiap post)

Total 11 query! Kalau ada 1.000 post? Maka 1.001 query. Inilah masalah N+1 query.


Diagram Alur N+1 Query (Tanpa Dataloader)

Mari visualisasikan bagaimana proses ini berjalan:

sequenceDiagram
    participant Client
    participant API
    participant DB

    Client->>API: Query: posts { author { ... } }
    API->>DB: SELECT * FROM posts
    API->>DB: SELECT * FROM users WHERE id = X (for post 1)
    API->>DB: SELECT * FROM users WHERE id = Y (for post 2)
    API->>DB: ...

Terlihat bahwa untuk tiap post, satu query ke table user dieksekusi.


Solusi: DataLoader

dataloader adalah library buatan tim Facebook yang mengimplementasikan batching dan caching untuk data fetch, sangat cocok untuk GraphQL resolver.

Filosofinya sederhana: Alih-alih melakukan query banyak kali secara terpisah, batch permintaan dengan key yang sama dalam satu request, dan lakukan satu kali query. Selain itu, hasil batch juga di-cache per request, sehingga tidak mengulang query untuk key yang sama.


Cara Kerja DataLoader

Berikut ilustrasi alurnya:

flowchart LR
A[GraphQL Resolver] -->|Meminta data by ID| B[DataLoader]
B -->|Batch collect ID| C{Sudah di-cache?}
C -- Tidak --> D[Query database with "WHERE id IN (...)" ]
D --> E[Map hasil ke masing-masing ID]
C -- Ya --> F[Kembalikan dari cache]
E --> G[Kirim ke Resolver]
F --> G

Implementasi DataLoader pada Resolver

Langkah-langkah sederhana:

  1. Buat instance DataLoader per request (agar cache per-user request, bukan global),
  2. Buat batch function (biasanya function yang menerima array ID dan return promise array data yang urutan sama dengan input ID),
  3. Gunakan DataLoader di resolver yang membutuhkan.

Instalasi

npm install dataloader

Batch Function

const DataLoader = require('dataloader');
const UserModel = require('./user-model');

// batchFn menerima array dari userIds, mengembalikan Promise<array user>
const batchGetUsers = async (userIds) => {
  /* Query sekaligus semua user dengan id di userIds */
  const users = await UserModel.findAll({
    where: { id: userIds }
  });

  // Pastikan urutan sama seperti userIds dari input
  return userIds.map(id => users.find(u => u.id === id));
};

Membuat DataLoader per Request

Pada konteks GraphQL server (misal Apollo Server):

const createLoaders = () => ({
  userLoader: new DataLoader(batchGetUsers),
});

// di setup context Apollo:
const server = new ApolloServer({
  typeDefs, resolvers,
  context: () => ({
    loaders: createLoaders()
  }),
});

Memakai DataLoader pada Resolver

const resolvers = {
  Query: {
    posts: () => PostModel.findAll(),
  },
  Post: {
    author: (post, _, { loaders }) => loaders.userLoader.load(post.authorId),
  }
};

Dengan ini, walaupun client minta 1.000 post, dan masing-masing butuh author, akan ada hanya dua query:

  • Satu query ke posts (SELECT * FROM posts)
  • Satu query ke users (SELECT * FROM users WHERE id IN (…))

Simulasi: Perbandingan Tanpa vs Dengan DataLoader

SkenarioJumlah PostJumlah Request ke DB (tanpa DataLoader)Dengan DataLoader
Simple51 + 5 = 62
Medium1001 + 100 = 1012
Extreme10001 + 1000 = 10012

Hemat query hingga 99%!


Studi Kasus: Simulasi Output

Mari kita test dua kali GET request tanpa dan dengan DataLoader.

Tanpa DataLoader (Pseudo-Log)

Query: posts { author { name } }
[QUERY] SELECT * FROM posts;
[QUERY] SELECT * FROM users WHERE id = 101;
[QUERY] SELECT * FROM users WHERE id = 102;
[QUERY] SELECT * FROM users WHERE id = 103;
[QUERY] ... sebanyak N (jumlah post)

Dengan DataLoader (Pseudo-Log)

Query: posts { author { name } }
[QUERY] SELECT * FROM posts;
[QUERY] SELECT * FROM users WHERE id IN (101, 102, 103, ... N);

Hanya dua query, apa pun nilai N! Database load merosot drastis.


Kelebihan & Catatan Implementasi DataLoader

Kelebihan

  • Efisien: Query database jauh lebih sedikit.
  • Cache Per Request: Aman dari cache pollution antar user/request, thread-safe.
  • Deklaratif: Cukup ganti di resolver, API tetap agnostik.

Catatan

  • Batch Query Needs Mapping: Perlu mapping output data ke order input untuk menjaga hasil correct.
  • Cache per Request, Bukan Global: Untuk jaga data antar user tetap aman.

Best Practice

  1. Buat DataLoader di Context: Jangan jadikan global singleton.
  2. Batchkan Query Intensif: Fokus dulu ke resolver yang sering kena N+1, misal relasi parent-child.
  3. Monitor Query Patterns: Gunakan logging pada layer ORM/database untuk mendeteksi query meledak.

Penutup

Masalah N+1 query bukan hanya isapan jempol saat membuat GraphQL API. Ia bisa tersembunyi di balik kemudahan pola resolver GraphQL. Namun, dengan mengadopsi dataloader, kita bisa mengoptimasi query resolver dengan sangat mudah, bahkan tanpa harus merombak model data kita. Hanya sedikit perubahan pada resolver dan context, performa API naik, beban database lebih ringan, dan kode lebih maintainable.

Jangan cuma kenali masalahnya, praktikkan optimasinya!
Selamat mencoba, semoga aplikasi GraphQL-mu makin kencang 🚀


Referensi:

comments powered by Disqus