1. Pengenalan Drizzle ORM
Drizzle ORM adalah ORM (Object-Relational Mapping) ringan dan modern yang dirancang khusus untuk TypeScript. Berbeda dari ORM tradisional seperti Prisma atau TypeORM, Drizzle mengambil pendekatan yang sangat berbeda — ia menawarkan API yang sangat mendekati SQL asli namun tetap mendapatkan keuntungan penuh dari type safety TypeScript.
Drizzle ORM dibangun dengan filosofi "SQL-like" yang artinya query yang kamu tulis akan terasa seperti menulis SQL biasa, tetapi dengan keamanan tipe (type safety) dari TypeScript yang kuat. Ini menjadikan Drizzle pilihan yang sangat baik untuk developer yang menginginkan kontrol penuh atas query tanpa mengorbankan fitur ORM.
Mengapa Drizzle ORM?
| Fitur | Drizzle ORM | Prisma | TypeORM |
|---|---|---|---|
| Pendekatan | SQL-like, code-first | Schema-first | Decorator-based |
| Performance | ⚡ Sangat cepat | Sedang | Sedang |
| Type Safety | ⭐⭐⭐ Sangat kuat | ⭐⭐⭐ Sangat kuat | ⭐⭐ Cukup |
| Bundle Size | ~30KB | ~5MB (engine) | ~2MB |
| Database Support | PostgreSQL, MySQL, SQLite | Banyak | Banyak |
| Learning Curve | Rendah (jika tahu SQL) | Rendah | Sedang |
Jika kamu sudah familiar dengan SQL, Drizzle ORM akan terasa sangat natural karena API-nya dirancang menyerupai sintaks SQL yang sebenarnya.
Fitur Utama Drizzle ORM
- Type-safe queries — Setiap query memiliki tipe return yang otomatis diturunkan dari schema
- SQL-like API — API yang mirip SQL, memudahkan transisi dari raw SQL
- No code generation — Tidak perlu generate code, langsung gunakan schema
- Lightweight — Bundle size kecil, tidak ada engine besar
- Zero dependencies — Sangat sedikit dependency eksternal
- Full SQL support — Dukungan penuh terhadap fitur SQL yang kompleks
- Drizzle Studio — GUI untuk menjelajahi dan mengedit data
- Push & Migrate — Tools untuk push schema dan jalankan migrasi
2. Instalasi & Setup
Mari kita mulai dengan menginstal Drizzle ORM dan menyiapkan proyek TypeScript pertama. Kita akan menggunakan PostgreSQL sebagai database, tetapi kamu bisa menggantinya dengan MySQL atau SQLite sesuai kebutuhan.
Proyek Baru dengan Drizzle
# Buat direktori proyek mkdir belajar-drizzle cd belajar-drizzle # Inisialisasi proyek Node.js npm init -y # Instal TypeScript dan dependencies npm install -D typescript @types/node tsx # Instal Drizzle ORM dan driver PostgreSQL npm install drizzle-orm pg # Instal Drizzle Kit untuk CLI & migrasi npm install -D drizzle-kit # Inisialisasi TypeScript npx tsc --init
Koneksi Database
Buat file src/db/index.ts untuk mengatur koneksi ke database PostgreSQL:
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
// Buat connection pool ke PostgreSQL
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 5000,
});
// Inisialisasi Drizzle ORM dengan schema
export const db = drizzle(pool, { schema });
Drizzle Config
Buat file drizzle.config.ts di root proyek:
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
verbose: true,
strict: true,
});
Jangan pernah hardcode database URL di kode. Selalu gunakan environment variable DATABASE_URL untuk keamanan.
3. Definisi Schema
Schema di Drizzle ORM didefinisikan langsung menggunakan TypeScript. Ini berbeda dari Prisma yang menggunakan bahasa schema terpisah (Prisma Schema Language). Kelebihannya adalah tidak perlu code generation — kamu langsung mendapatkan type safety dari definisi schema yang ada.
Definisi Tabel Dasar
Buat file src/db/schema.ts:
import {
pgTable,
serial,
varchar,
text,
integer,
boolean,
timestamp,
pgEnum,
} from 'drizzle-orm/pg-core';
// Enum untuk status pengguna
export const userStatusEnum = pgEnum('user_status', [
'active',
'inactive',
'banned',
]);
// Definisi tabel users
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
passwordHash: text('password_hash').notNull(),
age: integer('age'),
isActive: boolean('is_active').default(true).notNull(),
status: userStatusEnum('status').default('active').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
Tipe Data di Drizzle
| Drizzle Type | SQL Type | Contoh |
|---|---|---|
serial |
SERIAL / Auto-increment | serial('id').primaryKey() |
varchar |
VARCHAR(n) | varchar('name', { length: 255 }) |
text |
TEXT | text('content') |
integer |
INTEGER | integer('count') |
boolean |
BOOLEAN | boolean('is_active') |
timestamp |
TIMESTAMP | timestamp('created_at') |
real |
REAL / FLOAT | real('price') |
jsonb |
JSONB | jsonb('metadata') |
uuid |
UUID | uuid('id').defaultRandom() |
pgEnum |
ENUM | pgEnum('status', [...]) |
Tabel dengan Kolom Tambahan
// Definisi tabel posts
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 500 }).notNull(),
slug: varchar('slug', { length: 500 }).notNull().unique(),
content: text('content'),
excerpt: text('excerpt'),
isPublished: boolean('is_published').default(false).notNull(),
viewCount: integer('view_count').default(0).notNull(),
authorId: integer('author_id').notNull()
.references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// Definisi tabel tags
export const tags = pgTable('tags', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull().unique(),
slug: varchar('slug', { length: 100 }).notNull().unique(),
});
Indeks dan Constraints
import { pgTable, serial, varchar, index, uniqueIndex } from 'drizzle-orm/pg-core';
export const products = pgTable('products', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
sku: varchar('sku', { length: 50 }).notNull(),
category: varchar('category', { length: 100 }),
price: integer('price').notNull(),
}, (table) => {
return {
// Single column index
nameIdx: index('products_name_idx').on(table.name),
// Unique index untuk SKU
skuIdx: uniqueIndex('products_sku_idx').on(table.sku),
// Composite index
categoryPriceIdx: index('products_cat_price_idx')
.on(table.category, table.price),
};
});
4. CRUD Queries
Drizzle ORM menawarkan dua cara untuk melakukan query: Query Builder (SQL-like) dan Relational Queries (seperti Prisma). Pada bagian ini kita akan fokus pada Query Builder yang merupakan pendekatan utama Drizzle.
Select — Membaca Data
import { db } from './db';
import { users, posts } from './db/schema';
import { eq, and, or, like, gte, lte, desc, sql } from 'drizzle-orm';
// SELECT * FROM users
const allUsers = await db.select().from(users);
// SELECT id, name, email FROM users
const partialUsers = await db
.select({
id: users.id,
name: users.name,
email: users.email,
})
.from(users);
// SELECT * FROM users WHERE id = 1
const user = await db
.select()
.from(users)
.where(eq(users.id, 1))
.limit(1);
// SELECT * FROM users WHERE is_active = true AND age >= 18
const activeAdults = await db
.select()
.from(users)
.where(
and(
eq(users.isActive, true),
gte(users.age, 18)
)
);
Filter dengan Operators
import { eq, ne, gt, gte, lt, lte, like, ilike,
inArray, notInArray, isNull, isNotNull,
and, or, not, between } from 'drizzle-orm';
// WHERE name LIKE '%john%' (case insensitive)
const searchResults = await db.select().from(users)
.where(ilike(users.name, '%john%'));
// WHERE id IN (1, 2, 3, 4)
const selectedUsers = await db.select().from(users)
.where(inArray(users.id, [1, 2, 3, 4]));
// WHERE age BETWEEN 18 AND 30
const youngAdults = await db.select().from(users)
.where(between(users.age, 18, 30));
// WHERE is_active = true OR status = 'banned'
const specialUsers = await db.select().from(users)
.where(or(
eq(users.isActive, true),
eq(users.status, 'banned')
));
// Complex: WHERE (age > 18 AND is_active = true) OR status = 'banned'
const complex = await db.select().from(users)
.where(or(
and(gt(users.age, 18), eq(users.isActive, true)),
eq(users.status, 'banned')
));
Insert — Menambahkan Data
// INSERT INTO users (name, email, password_hash, age)
// VALUES ('Budi', 'budi@email.com', 'hashed_pw', 25)
const newUser = await db.insert(users).values({
name: 'Budi',
email: 'budi@email.com',
passwordHash: 'hashed_password_here',
age: 25,
}).returning();
// returning() mengembalikan baris yang baru di-insert
console.log(newUser[0].id); // ID dari user yang baru dibuat
// Insert multiple rows sekaligus
const batchInsert = await db.insert(users).values([
{ name: 'Andi', email: 'andi@mail.com', passwordHash: 'pw1', age: 22 },
{ name: 'Sari', email: 'sari@mail.com', passwordHash: 'pw2', age: 28 },
{ name: 'Dewi', email: 'dewi@mail.com', passwordHash: 'pw3', age: 19 },
]).returning();
// INSERT ON CONFLICT (Upsert)
const upserted = await db.insert(users).values({
name: 'Budi Updated',
email: 'budi@email.com',
passwordHash: 'new_hash',
}).onConflictDoUpdate({
target: users.email,
set: { name: 'Budi Updated' },
}).returning();
Update — Mengubah Data
// UPDATE users SET name = 'Budi Santoso' WHERE id = 1
const updated = await db.update(users)
.set({
name: 'Budi Santoso',
updatedAt: new Date(),
})
.where(eq(users.id, 1))
.returning();
// Update dengan raw SQL expression
const viewIncrement = await db.update(posts)
.set({
viewCount: sql`${posts.viewCount} + 1`,
})
.where(eq(posts.id, 1))
.returning();
// Update semua yang belum publish
await db.update(posts)
.set({ isPublished: true })
.where(eq(posts.isPublished, false));
Delete — Menghapus Data
// DELETE FROM users WHERE id = 1
const deleted = await db.delete(users)
.where(eq(users.id, 1))
.returning();
// DELETE dengan kondisi kompleks
await db.delete(users)
.where(
and(
eq(users.isActive, false),
lt(users.createdAt, new Date('2025-01-01'))
)
);
Sorting & Pagination
import { desc, asc } from 'drizzle-orm';
// ORDER BY created_at DESC LIMIT 10
const recentPosts = await db.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(10);
// Pagination: LIMIT dan OFFSET
const page = 2;
const pageSize = 10;
const paginated = await db.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(pageSize)
.offset((page - 1) * pageSize);
// ORDER BY dengan multiple kolom
const sorted = await db.select()
.from(posts)
.orderBy(
desc(posts.isPublished),
desc(posts.viewCount),
asc(posts.title)
);
Aggregation & Grouping
import { count, sum, avg, min, max } from 'drizzle-orm';
// COUNT(*) FROM users
const totalUsers = await db
.select({ count: count() })
.from(users);
// GROUP BY status
const statusCounts = await db
.select({
status: users.status,
count: count(),
})
.from(users)
.groupBy(users.status);
// HAVING: filter setelah grouping
const activeStatuses = await db
.select({
status: users.status,
count: count(),
})
.from(users)
.groupBy(users.status)
.having(gt(count(), 5));
// Aggregation functions
const stats = await db
.select({
totalPosts: count(posts.id),
totalViews: sum(posts.viewCount),
avgViews: avg(posts.viewCount),
maxViews: max(posts.viewCount),
})
.from(posts);
5. Relasi Antar Tabel
Drizzle mendefinisikan relasi menggunakan relations(). Relasi tidak menambah kolom baru ke tabel, melainkan mendefinisikan cara tabel-tabel saling berhubungan. Kamu bisa mendefinisikan relasi one-to-one, one-to-many, dan many-to-many.
One-to-Many (User → Posts)
import { relations } from 'drizzle-orm';
// User memiliki banyak posts
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
// Post dimiliki oleh satu user
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
Many-to-Many (Posts ↔ Tags)
// Tabel junction untuk relasi many-to-many
export const postsToTags = pgTable('posts_to_tags', {
postId: integer('post_id').notNull()
.references(() => posts.id, { onDelete: 'cascade' }),
tagId: integer('tag_id').notNull()
.references(() => tags.id, { onDelete: 'cascade' }),
});
// Relasi
export const postsToTagsRelations = relations(postsToTags, ({ one }) => ({
post: one(posts, {
fields: [postsToTags.postId],
references: [posts.id],
}),
tag: one(tags, {
fields: [postsToTags.tagId],
references: [tags.id],
}),
}));
export const postsRelationsUpdated = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
postsToTags: many(postsToTags),
}));
export const tagsRelations = relations(tags, ({ many }) => ({
postsToTags: many(postsToTags),
}));
Relational Query API
Setelah mendefinisikan relasi, kamu bisa menggunakan db.query untuk melakukan nested queries:
// Ambil user beserta semua posts-nya
const userWithPosts = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: true,
},
});
// Hasil: { id: 1, name: 'Budi', posts: [{ id: 1, title: '...', ... }] }
// Nested relations: user → posts → tags
const userFull = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: {
with: {
postsToTags: {
with: {
tag: true,
},
},
},
where: eq(posts.isPublished, true),
},
},
});
// findMany dengan filter
const activeUsersWithPosts = await db.query.users.findMany({
where: eq(users.isActive, true),
with: {
posts: {
limit: 5,
orderBy: desc(posts.createdAt),
},
},
});
6. Migrasi Database
Drizzle Kit menyediakan tools untuk mengelola migrasi database secara terstruktur. Kamu memiliki dua pilihan: migrate (file-based migrations) atau push (langsung apply ke database).
Generate Migrasi
# Generate file migrasi SQL dari perubahan schema npx drizzle-kit generate # Output: ./drizzle/0000_initial.sql # File berisi CREATE TABLE, ALTER TABLE, dll. # Push langsung ke database (untuk development) npx drizzle-kit push # Jalankan migrasi ke database npx drizzle-kit migrate # Lihat perbedaan antara schema dan database npx drizzle-kit diff # Drop semua tabel (hati-hati!) npx drizzle-kit drop
Cara Migrasi Bekerja
Ketika kamu mengubah schema, Drizzle Kit akan mendeteksi perbedaan dan membuat file SQL migrasi. File ini bisa di-commit ke version control dan dijalankan secara otomatis di production:
-- drizzle/0000_initial.sql
CREATE TABLE IF NOT EXISTS "users" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar(255) NOT NULL,
"email" varchar(255) NOT NULL,
"password_hash" text NOT NULL,
"age" integer,
"is_active" boolean DEFAULT true NOT NULL,
"status" user_status DEFAULT 'active' NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS "users_email_unique"
ON "users" ("email");
CREATE TYPE "user_status" AS ENUM ('active', 'inactive', 'banned');
Menjalankan Migrasi di Kode
import { migrate } from 'drizzle-orm/node-postgres/migrator';
import { db } from './db';
// Jalankan semua file migrasi di folder ./drizzle
async function runMigrations() {
console.log('Menjalankan migrasi...');
await migrate(db, { migrationsFolder: './drizzle' });
console.log('Migrasi selesai!');
}
runMigrations().catch(console.error);
Gunakan drizzle-kit push untuk development (cepat & langsung) dan drizzle-kit generate + migrate untuk production (aman & terkontrol).
7. Transactions
Transactions memungkinkan kamu menjalankan beberapa operasi database secara atomik — semua berhasil atau semua gagal. Ini sangat penting untuk menjaga konsistensi data.
Basic Transaction
// Transaction: buat user dan post pertama sekaligus
const result = await db.transaction(async (tx) => {
// Step 1: Buat user baru
const newUser = await tx.insert(users).values({
name: 'Budi',
email: 'budi@email.com',
passwordHash: 'hashed_pw',
age: 25,
}).returning();
// Step 2: Buat post pertama untuk user tersebut
const newPost = await tx.insert(posts).values({
title: 'Post Pertama',
slug: 'post-pertama',
content: 'Hello World!',
authorId: newUser[0].id,
}).returning();
// Kembalikan hasil
return { user: newUser[0], post: newPost[0] };
});
console.log(result.user.id);
console.log(result.post.title);
Transaction dengan Savepoints
const result = await db.transaction(async (tx) => {
// Operasi pertama selalu dijalankan
const user = await tx.insert(users).values({
name: 'Sari',
email: 'sari@email.com',
passwordHash: 'pw',
}).returning();
try {
// Operasi yang mungkin gagal
await tx.insert(posts).values({
title: 'Post Sari',
slug: 'post-sari',
authorId: user[0].id,
});
} catch (error) {
// Jika gagal, transaction tetap berjalan
console.log('Gagal membuat post, tapi user tetap dibuat');
}
return user[0];
});
Isolation Levels
// Transaction dengan isolation level khusus
await db.transaction(async (tx) => {
// Set isolation level untuk transaction ini
await tx.execute(
sql`SET TRANSACTION ISOLATION LEVEL SERIALIZABLE`
);
// Operasi database yang sensitif terhadap concurrency
const balance = await tx.select()
.from(accounts)
.where(eq(accounts.id, 1));
await tx.update(accounts)
.set({ balance: balance[0].balance - 100 })
.where(eq(accounts.id, 1));
});
8. Drizzle Kit CLI
Drizzle Kit adalah CLI yang mendampingi Drizzle ORM untuk mengelola schema, migrasi, dan visualisasi data. Berikut adalah semua command yang tersedia:
| Command | Fungsi |
|---|---|
drizzle-kit generate |
Generate file SQL migrasi dari perubahan schema |
drizzle-kit migrate |
Jalankan file migrasi ke database |
drizzle-kit push |
Push langsung schema ke database (dev only) |
drizzle-kit pull |
Pull schema dari database yang sudah ada (introspection) |
drizzle-kit studio |
Buka Drizzle Studio (GUI) di browser |
drizzle-kit drop |
Hapus file migrasi |
drizzle-kit diff |
Tampilkan perbedaan schema vs database |
drizzle-kit check |
Periksa konsistensi file migrasi |
Drizzle Studio
Drizzle Studio adalah GUI interaktif yang memungkinkan kamu melihat, mengedit, dan mengelola data di browser:
# Buka Drizzle Studio npx drizzle-kit studio # Output: # Drizzle Studio is running # URL: https://local.drizzle.studio
Introspection (Pull Schema)
Jika kamu sudah memiliki database yang sudah ada dan ingin membuat Drizzle schema darinya:
# Pull schema dari database yang sudah ada npx drizzle-kit pull # Ini akan generate file schema.ts otomatis # berdasarkan struktur database yang ada
9. Best Practices
Struktur Folder
src/
db/
index.ts # Koneksi database
schema/
users.ts # Schema tabel users
posts.ts # Schema tabel posts
relations.ts # Definisi relasi
index.ts # Export semua schema
services/
user.service.ts # Query functions untuk users
post.service.ts # Query functions untuk posts
drizzle/
0000_initial.sql # File migrasi
0001_add_tags.sql
drizzle.config.ts
Separate Schema Files
// src/db/schema/users.ts
import { pgTable, serial, varchar, boolean, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }).notNull().unique(),
isActive: boolean('is_active').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
// src/db/schema/index.ts
export * from './users';
export * from './posts';
export * from './tags';
export * from './relations';
Reusable Query Functions
// src/services/user.service.ts
import { db } from '../db';
import { users } from '../db/schema';
import { eq, and } from 'drizzle-orm';
export const UserService = {
async findById(id: number) {
return db.query.users.findFirst({
where: eq(users.id, id),
});
},
async findWithPosts(id: number) {
return db.query.users.findFirst({
where: eq(users.id, id),
with: { posts: true },
});
},
async create(data: { name: string; email: string; passwordHash: string }) {
const result = await db.insert(users).values(data).returning();
return result[0];
},
async deactivate(id: number) {
const result = await db.update(users)
.set({ isActive: false })
.where(eq(users.id, id))
.returning();
return result[0];
},
};
Selalu gunakan parameterized query dari Drizzle ORM dan hindari interpolasi string langsung untuk mencegah SQL injection. Drizzle sudah menangani ini secara otomatis, jadi jangan bypass fitur keamanan ini.
Tips Performance
- Gunakan select spesifik — Jangan selalu
select().from(), pilih kolom yang dibutuhkan saja - Manfaatkan indeks — Tambahkan indeks pada kolom yang sering di-query
- Pagination — Selalu gunakan
limit()danoffset()untuk query besar - Connection pooling — Atur max connections di Pool dengan benar
- Bulk operations — Gunakan batch insert/update untuk operasi massal
- Monitor queries — Gunakan logging untuk memantau query lambat
10. Quiz Pemahaman
Uji pemahamanmu tentang Drizzle ORM dengan menjawab pertanyaan berikut: