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:
- Buat instance DataLoader per request (agar cache per-user request, bukan global),
- Buat batch function (biasanya function yang menerima array ID dan return promise array data yang urutan sama dengan input ID),
- 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
| Skenario | Jumlah Post | Jumlah Request ke DB (tanpa DataLoader) | Dengan DataLoader |
|---|---|---|---|
| Simple | 5 | 1 + 5 = 6 | 2 |
| Medium | 100 | 1 + 100 = 101 | 2 |
| Extreme | 1000 | 1 + 1000 = 1001 | 2 |
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
- Buat DataLoader di Context: Jangan jadikan global singleton.
- Batchkan Query Intensif: Fokus dulu ke resolver yang sering kena N+1, misal relasi parent-child.
- 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: