Kenapa Gue Stop Pakai ORM: 3 Tahun Pake, 2 Tahun Suffering

Query invoice yang seharusnya 50ms jadi 8 detik karena Prisma N+1. ORM bikin lo produktif di awal, tapi ngunci lo pas skala naik. Pengalaman 3 tahun Prisma + TypeORM.

· · 10 min read

Gue pernah bikin fitur "list invoice yang belum dibayar bulan ini." Simple query. Pake Prisma. Code-nya clean banget:

const invoices = await prisma.invoice.findMany({
where: {
status: "unpaid",
createdAt: { gte: startOfMonth, lt: endOfMonth }
},
include: { customer: true, items: true }
})

Jalan. 200ms. Fine. Tapi pas production, data naik ke 50.000 invoice, query naik jadi 8 detik. Gue inspection - ternyata Prisma fetch semua row dulu, baru filter di application layer buat include. N+1 problem klasik.

Gue coba optimize pake raw query dalam Prisma. 50ms. Sama sekali beda. Saat itu gue sadar: ORM bikin gue cepet nulis kode, tapi bikin gue lambat pas production. Gue berhenti pakai ORM setelah itu.

Ini bukan artikel "ORM itu jelek." ORM itu alat. Tapi setelah 3 tahun pake dan 2 tahun suffering akibatnya, gue punya opini kuat: ORM bikin lo produktif di awal, tapi ngunci lo pas skala naik. Ini pengalaman gue.

---

ORM Itu Apa Sebenernya?

ORM = Object-Relational Mapper. Alat yang nge-map tabel database jadi object di kode lo. Lo gak nulis SQL, lo tulis code. Contoh Prisma (TypeScript):

// ORM way
const user = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true }
})

-- SQL way
SELECT u.*, p.*
FROM users u
LEFT JOIN posts p ON p.user_id = u.id
WHERE u.id = 1

Hasilnya sama. Bedanya: ORM "ngomong" kayak developer (object, method, autocomplete). SQL "ngomong" langsung sama database.

Filosofi ORM: abstraksi. Lo gak perlu paham SQL, lo cukup paham object. Tapi setiap abstraksi punya harga.

---

Kenapa ORM Bikin Cepet di Awal

1. Autocomplete + type safety.

Lo tulis prisma.user.find dan IDE ngasih saran. Gak perlu ingat nama kolom. Ganti schema? Type error muncul, bukan runtime error di production. Ini betul-betul加速 productivity 2-3x di early development.

2. Migration otomatis.

Ubah schema.prisma, jalankan prisma migrate dev, database ke-update. Gak perlu tulis SQL migration manual. Buat MVP, ini sangat cepat.

3. Cross-database compatibility.

Lo tulis code sekali, jalan di Postgres, MySQL, SQLite. Pindah DB? Ganti datasource di config. (Spoiler: gue gak pernah butuh ini di production beneran.)

4. Model relationship jelas.

user.posts - gampang dibaca. SQL JOIN butuh mikir. Buat yang baru belajar, ORM lowering barrier.

Itu benefit nyata. Gue gak ngeraguin itu. Tapi semua itu punya sisi gelap yang baru keliatan pas skala naik.

---

Sisi Gelap yang Landing Page Gak Bilang

1. N+1 problem - musuh utama ORM.

Contoh paling klasik:

// Ambil 100 user
const users = await prisma.user.findMany()
// Loop, ambil posts tiap user
for (const u of users) {
const posts = await prisma.post.findMany({ where: { userId: u.id } })
// ...
}

Kode-nya kelihatan innocent. Tapi eksekusinya: 1 query buat user + 100 query buat posts = 101 query. Database overload.

SQL way: 1 query pake JOIN. Done. ORM bisa diakalin pake include atau eager loading, tapi tetep generate SQL yang kadang gak optimal.

2. Lo gak ngerti SQL, dan itu bahaya.

ORM ngajarin lo "think in objects," bukan "think in sets." SQL itu set-based thinking - "ambil semua row yang match kondisi X, group by Y, aggregate Z." Beda mental model.

Pas performance issue muncul, lo gak bisa debug karena lo gak ngerti SQL yang di-generate ORM. Lo gak bisa EXPLAIN ANALYZE. Lo gak bisa optimasi index. Lo terjebak di "code gue udah bener, kenapa lambat?"

3. Generated SQL sering gak optimal.

ORM harus handle semua kasus, jadi SQL yang di-generate sering over-generalized. Subquery yang gak perlu. JOIN yang seharusnya pake LEFT tapi ORM pake INNER karena default. Lo gak punya kontrol halus.

Contoh: Prisma include sering generate query dengan multiple LEFT JOIN yang bisa diganti EXISTS subquery. Lebih cepet 10x. Tapi lo gak bisa make Prisma API buat nulis EXISTS. Harus bypass ke raw query.

4. Abstraction leak saat skala naik.

ORM asumsi lo bekerja dengan row-level: ambil 1 user, update 1 user. Tapi pas production, lo butuh:

-- Bulk update 100.000 row
UPDATE users SET status = 'inactive' WHERE last_login = $1 AND i.created_at < $2
GROUP BY i.id, c.name

ORM gak akan generate ini. Mereka generate multiple query atau subquery yang gak perlu.

3. Migration jadi more deliberate.

Tiap schema change harus ditulis SQL manual. Lo mikir 2x sebelum tambah kolom, ubah tipe, dsb. Migrasi jadi lebih stabil.

4. Type safety tanpa ORM magic.

Kysely ngasih type safety di query builder, tanpa abstraction layer. Lo tulis SQL, IDE ngasih type error kalau kolom salah. Best of both worlds.

---

Quick Comparison

| | ORM (Prisma, TypeORM) | Raw SQL / Query Builder |
|---|---|---|
| Speed to write (early dev) | Cepet (autocomplete, migration) | Lambat (tulis SQL manual) |
| Speed at scale | Sering lambat (N+1, suboptimal query) | Optimal (lo control) |
| Learning curve | Rendah | Sedang (butuh paham SQL) |
| Debugging | Susah (ORM-generated query mystery) | Gampang (lo tau SQL-nya) |
| Type safety | Ya (generally) | Ya (kalo pake kysely/sqlx) |
| Migration | Otomatis, sering permissive | Manual, lebih deliberate |
| Best for | MVP, CRUD apps | Production, skala gede, complex query |

---

Yang Sering Disalahpahami

"ORM bikin lo lebih produktif selalu." - Enggak. Di awal iya. Pas skala naik, lo bisa lebih lambat karena debug ORM-generated query yang gak optimal.

"SQL itu susah." - ENGGA. SQL itu set-based thinking, beda mental model sama OOP. Sekali lo paham JOIN, GROUP BY, subquery, lo bisa bikin query yang ORM gak bisa express.

"ORM lebih aman dari SQL injection." - Modern query builder juga parameterized. SQL injection muncul kalau lo string-concat, bukan karena SQL itu insecure.

"Pake ORM = gak perlu paham SQL." - Fatal. Lo tetep harus paham SQL untuk debug, optimize, dan architected skala. ORM itu tools, bukan pengganti knowledge.

---

Penutup yang Jujur

ORM bukan musuh. Gue masih pake buat prototype internal, script one-off, MVP. Tapi buat production code yang skala, gue pindah ke SQL murni (atau query builder ringan kayak kysely). Soalnya: production butuh kontrol, performance, dan debuggability yang ORM susah kasih.

Filosofi yang gue pelajarin: abstraksi itu berguna kalau lo paham yang diabstraksi. Lo gak bisa efektif pakai ORM kalau lo gak paham SQL yang diabstraksi. Lo cuma bisa debug ORM kalau lo baca generated SQL-nya. Lo cuma bisa optimasi kalau lo ngerti execution plan.

Mau pakai ORM? Boleh. Tapi pelajari SQL juga. Baca query yang ORM generate. Belajar EXPLAIN ANALYZE. Begitu lo ngemengerti apa yang terjadi di bawah hood, lo bisa milih: kapan pakai ORM, kapan fallback ke raw SQL. Itu yang bikin lo dev senior.

Kalau lo cuma andalkan ORM tanpa ngerti SQL, lo bakal stuck pas skala naik. Kayak gue dulu, 8 detik query yang seharusnya 50ms.