Lewati ke konten utama
menjadi.dev
Chapter 17.12 Backend Lanjutan

Drizzle ORM: Advanced Queries dan Best Practices

Query kompleks, performance, dan pattern production-ready

Tujuan Pembelajaran

  • Bisa menulis query kompleks dengan Drizzle
  • Mengerti transaction dan batch queries
  • Mengerti pattern repository dan service layer

Analogi

Diagram

      Advanced Patterns:
├── Transactions (db.transaction)
├── Batch inserts (db.insertAll)
├── Subqueries
├── CTE (Common Table Expressions)
└── Repository Pattern
    

Production code membutuhkan pattern yang terstruktur dan query yang efisien

Penjelasan Konsep

Setelah menguasai CRUD dasar dan migrations, saatnya mengeksplor teknik advanced yang dibutuhkan aplikasi production.

Transactions

Transaction memastikan beberapa operasi database berhasil SEMUA atau GAGAL SEMUA — tidak ada setengah-setengah.

Ini penting untuk operasi finansial, inventory, atau data yang saling bergantung.

await db.transaction(async (tx) => {
  // Kurangi stok
  await tx.update(products)
    .set({ stock: sql`${products.stock} - 1` })
    .where(eq(products.id, productId));

  // Buat order
  const order = await tx.insert(orders).values({ productId, userId }).returning();

  // Kalau salah satu gagal, semua di-rollback
});

Raw SQL Fallback

Untuk query yang terlalu kompleks untuk query builder, gunakan raw SQL:

import { sql } from 'drizzle-orm';

const result = await db.run(sql`
  WITH RECURSIVE category_tree AS (
    SELECT id, name, parent_id, 0 as depth
    FROM categories WHERE parent_id IS NULL
    UNION ALL
    SELECT c.id, c.name, c.parent_id, ct.depth + 1
    FROM categories c
    JOIN category_tree ct ON c.parent_id = ct.id
  )
  SELECT * FROM category_tree
`);

Notice: sql tagged template literal — tetap type-safe untuk parameters!

Batch Operations

Untuk insert/update banyak data sekaligus:

// Batch insert (lebih efisien daripada insert satu-satu)
await db.insert(users).values([
  { name: 'User 1',

Email: 'u1@mail.com' },
  { name: 'User 2', email: 'u2@mail.com' },
  // ... 1000 users
]);

Repository Pattern

Untuk aplikasi besar, gunakan repository pattern untuk memisahkan data access logic:

// repositories/UserRepository.ts
export class UserRepository {
  async findById(id: number) {
    const [user] = await db.select().from(users).where(eq(users.id, id));
    return user ?? null;
  }

  async findByEmail(email: string) {
    const [user] = await db.select().from(users).where(eq(users.email, email));
    return user ?? null;
  }

  async create(data: NewUser) {
    const [user] = await db.insert(users).values(data).returning();
    return user;
  }
}

Prepared Statements

Drizzle secara otomatis menggunakan prepared statements untuk keamanan (mencegah SQL injection) dan performa (query plan di-cache oleh database).

Indexing

Tambahkan index di schema untuk query yang sering dijalankan:

export const users = sqliteTable('users', {
  id: integer('id').primaryKey(),
  email: text('email').notNull().unique(),
}, (table) => [
  index('email_idx').on(table.email),
]);

Contoh Kode

typescript
// Repository pattern implementation
import { eq, like, and, desc } from 'drizzle-orm';
import { db } from './db';
import { users, type User, type NewUser } from './schema';

export class UserRepository {
  async findAll(options: { search?: string; role?: string; limit?: number; offset?: number } = {}) {
    const { search, role, limit = 50, offset = 0 } = options;
    const conditions = [];
    
    if (search) conditions.push(like(users.name, `%${search}%`));
    if (role) conditions.push(eq(users.role, role));
    
    return db.select().from(users)
      .where(conditions.length ? and(...conditions) : undefined)
      .orderBy(desc(users.createdAt))
      .limit(limit)
      .offset(offset);
  }

  async findById(id: number): Promise<User | null> {
    const [user] = await db.select().from(users).where(eq(users.id, id));
    return user ?? null;
  }

  async findByEmail(email: string): Promise<User | null> {
    const [user] = await db.select().from(users).where(eq(users.email, email));
    return user ?? null;
  }

  async create(data: NewUser): Promise<User> {
    const [user] = await db.insert(users).values(data).returning();
    return user;
  }

  async update(id: number, data: Partial<NewUser>): Promise<User | null> {
    const [user] = await db.update(users).set(data).where(eq(users.id, id)).returning();
    return user ?? null;
  }

  async delete(id: number): Promise<void> {
    await db.delete(users).where(eq(users.id, id));
  }

  async exists(email: string): Promise<boolean> {
    const result = await db.select({ count: count() }).from(users).where(eq(users.email, email));
    return result[0].count > 0;
  }
}

// Usage in service layer
const userRepo = new UserRepository();
const user = await userRepo.findById(1);
if (!user) throw new HTTPException(404, { message: 'User not found' });

Penjelasan Kode

Repository pattern memisahkan data access dari business logic. Memudahkan testing dan refactoring. Null coalescing (??) untuk default value.

Prompt AI

Implementasikan Unit of Work pattern dengan Drizzle transactions. Semua operasi dalam satu request menggunakan transaction yang sama.

Pertanyaan Reflektif

Tambahkan query logging untuk development. Catat setiap query yang dijalankan beserta execution time-nya.