İçeriğe geç
Sedat Demir
Geri dön

Drizzle ORM Derinlemesine: TypeScript'in En İyi ORM'i mi?

Drizzle ORM Derinlemesine: TypeScript'in En İyi ORM'i mi?

TypeScript ekosisteminde ORM seçimi her zaman tartışmalı bir konu olmuştur. Prisma'nın deklaratif şema yaklaşımı, TypeORM'un decorator tabanlı yapısı ve Knex'in query builder felsefesi — her birinin güçlü ve zayıf yanları var. Ancak 2023'ten itibaren sahneye çıkan Drizzle ORM, "SQL bilen geliştiriciler için ORM" mottosuyla bu denklemi tamamen değiştirdi.

Bu yazıda Drizzle ORM'i derinlemesine inceleyecek, gerçek dünya kullanım senaryolarıyla birlikte neden bu kadar popüler olduğunu anlayacağız.


Drizzle ORM Nedir?

Drizzle ORM, TypeScript-first bir ORM ve query builder'dır. En temel felsefesi "If you know SQL, you know Drizzle" (SQL biliyorsan, Drizzle'ı da bilirsin) cümlesiyle özetlenebilir. Diğer ORM'lerin aksine, SQL'i soyutlamak yerine TypeScript ile SQL yazma deneyimini birebir yansıtmayı hedefler.

Drizzle'ın Öne Çıkan Özellikleri


Kurulum ve İlk Yapılandırma

Drizzle'ı bir projede kullanmaya başlamak oldukça basittir. PostgreSQL ile bir örnek yapalım:

npm install drizzle-orm postgres
npm install -D drizzle-kit

drizzle-kit, şema yönetimi ve migration'lar için kullanılan CLI aracıdır. Çalışma zamanında değil, yalnızca geliştirme ortamında ihtiyaç duyulur.

Yapılandırma Dosyası

Proje kökünde bir drizzle.config.ts dosyası oluşturun:

import { defineConfig } from "drizzle-kit";

export default defineConfig({
  schema: "./src/db/schema.ts",
  out: "./drizzle",
  dialect: "postgresql",
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

Şema (Schema) Tanımlama

Drizzle'da şemalar, TypeScript dosyalarında fonksiyonlar aracılığıyla tanımlanır. Bu yaklaşım Prisma'nın .prisma dosyasından temel bir farklılık taşır — şemanız zaten TypeScript'tir, dolayısıyla IDE desteği, import/export ve koşullu mantık gibi dil özelliklerini doğrudan kullanabilirsiniz.

// src/db/schema.ts
import {
  pgTable,
  serial,
  varchar,
  text,
  timestamp,
  integer,
  boolean,
  pgEnum,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

// Enum tanımı
export const roleEnum = pgEnum("role", ["admin", "editor", "user"]);

// Users tablosu
export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 255 }).notNull(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  role: roleEnum("role").default("user").notNull(),
  bio: text("bio"),
  isActive: boolean("is_active").default(true).notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at").defaultNow().notNull(),
});

// Posts tablosu
export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: varchar("title", { length: 500 }).notNull(),
  content: text("content").notNull(),
  published: boolean("published").default(false).notNull(),
  authorId: integer("author_id")
    .references(() => users.id, { onDelete: "cascade" })
    .notNull(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

// İlişki tanımları
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

Dikkat ederseniz her kolon adı için hem TypeScript property adını (isActive) hem de veritabanı kolon adını (is_active) ayrı ayrı belirtebiliyorsunuz. Bu, naming convention farklılıklarını zarif bir şekilde çözer.


Veritabanı Bağlantısı

// src/db/index.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

const connectionString = process.env.DATABASE_URL!;

// Connection pool
const client = postgres(connectionString, {
  max: 10,
  idle_timeout: 20,
});

export const db = drizzle(client, { schema });

Drizzle, birçok farklı PostgreSQL driver'ını destekler: postgres, node-postgres (pg), Neon Serverless, Supabase, Vercel Postgres ve daha fazlası.


Migration Yönetimi

Şemanızı tanımladıktan sonra migration dosyalarını oluşturmak tek komut:

npx drizzle-kit generate

Bu komut ./drizzle klasörü altında SQL migration dosyaları oluşturur. Migration'ları uygulamak için:

npx drizzle-kit migrate

Geliştirme aşamasında hızlıca şemayı veritabanına yansıtmak isterseniz:

npx drizzle-kit push

push komutu migration dosyası oluşturmadan doğrudan veritabanını günceller — prototipleme için idealdir ancak üretim ortamında generate + migrate akışı tercih edilmelidir.

Drizzle Studio

Drizzle'ın en sevilen özelliklerinden biri de yerleşik veritabanı tarayıcısıdır:

npx drizzle-kit studio

Bu komut tarayıcıda açılan bir GUI sunar. Tablolarınızı görüntüleyebilir, veri ekleyebilir, düzenleyebilir ve silebilirsiniz — herhangi bir ek araç kurmadan.


Sorgu Yazımı: İki Farklı API

Drizzle, iki farklı sorgu API'si sunar. Bu, onu diğer ORM'lerden ayıran en önemli özelliklerden biridir.

1. SQL-Like API (Select Builder)

SQL bilen geliştiriciler için son derece tanıdık bir söz dizimi:

import { eq, and, gt, like, desc, count, sql } from "drizzle-orm";
import { db } from "./db";
import { users, posts } from "./db/schema";

// Basit SELECT
const allUsers = await db.select().from(users);

// WHERE koşulu ile
const activeAdmins = await db
  .select()
  .from(users)
  .where(and(eq(users.role, "admin"), eq(users.isActive, true)));

// Belirli kolonları seçme
const userEmails = await db
  .select({
    id: users.id,
    email: users.email,
  })
  .from(users);

// JOIN sorgusu
const postsWithAuthors = await db
  .select({
    postTitle: posts.title,
    authorName: users.name,
    authorEmail: users.email,
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .where(eq(posts.published, true))
  .orderBy(desc(posts.createdAt))
  .limit(10);

// Aggregation
const postCountByUser = await db
  .select({
    authorId: posts.authorId,
    authorName: users.name,
    totalPosts: count(posts.id),
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .groupBy(posts.authorId, users.name);

// Raw SQL kullanımı
const result = await db.execute(
  sql`SELECT * FROM users WHERE created_at > NOW() - INTERVAL '7 days'`
);

Bu API'de her sorgu, yazılan SQL'in TypeScript karşılığı gibi okunur. Bir SQL sorgusunu Drizzle'a çevirmek (veya tam tersini yapmak) neredeyse mekanik bir süreçtir.

2. Relational Query API

Prisma benzeri, ilişki tabanlı sorgular için:

// Kullanıcıyı postlarıyla birlikte getir
const userWithPosts = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: {
    posts: {
      where: eq(posts.published, true),
      orderBy: [desc(posts.createdAt)],
      limit: 5,
    },
  },
});

// Tüm yayınlanmış postları yazarlarıyla birlikte getir
const publishedPosts = await db.query.posts.findMany({
  where: eq(posts.published, true),
  with: {
    author: true,
  },
  columns: {
    id: true,
    title: true,
    createdAt: true,
  },
  orderBy: [desc(posts.createdAt)],
  limit: 20,
  offset: 0,
});

Bu API, relations() tanımlarını kullanarak çalışır ve Prisma'dan geçiş yapan geliştiriciler için tanıdık bir deneyim sunar.


INSERT, UPDATE ve DELETE İşlemleri

// Tekil ekleme
const newUser = await db
  .insert(users)
  .values({
    name: "Ahmet Yılmaz",
    email: "ahmet@example.com",
    role: "editor",
  })
  .returning();

// Toplu ekleme
const newPosts = await db
  .insert(posts)
  .values([
    { title: "İlk Yazı", content: "Merhaba dünya!", authorId: 1 },
    { title: "İkinci Yazı", content: "Devam ediyoruz.", authorId: 1 },
  ])
  .returning();

// Upsert (conflict handling)
await db
  .insert(users)
  .values({ name: "Ahmet", email: "ahmet@example.com" })
  .onConflictDoUpdate({
    target: users.email,
    set: { name: "Ahmet Yılmaz" },
  });

// Güncelleme
await db
  .update(users)
  .set({ isActive: false, updatedAt: new Date() })
  .where(eq(users.id, 5));

// Silme
await db.delete(posts).where(eq(posts.authorId, 5));

Tüm bu işlemlerde dönen sonuçlar tam tip güvenliğine sahiptir. .returning() kullandığınızda, dönen objenin tipi şemanızdan otomatik olarak türetilir.


Drizzle vs Prisma: Karşılaştırma

Özellik Drizzle ORM Prisma
Şema tanımı TypeScript dosyası .prisma DSL
Bundle boyutu ~7.4KB ~2MB+ (Engine dahil)
Serverless uyumu Mükemmel Sorunlu (cold start)
SQL kontrolü Tam kontrol Sınırlı
Öğrenme eğrisi SQL bilgisi gerektirir Daha kolay başlangıç
Tip güvenliği Uçtan uca Uçtan uca
Raw SQL Doğal entegrasyon Mümkün ama ayrık
Migration SQL dosyaları Otomatik
Olgunluk Daha genç Daha olgun ekosistem

Ne Zaman Drizzle?

Ne Zaman Prisma?


Prepared Statements ve Performans

Drizzle, prepared statement desteğiyle tekrarlanan sorguların performansını artırır:

import { placeholder } from "drizzle-orm";

const getUserById = db
  .select()
  .from(users)
  .where(eq(users.id, placeholder("id")))
  .prepare("get_user_by_id");

// Kullanım — sorgu planı önbelleklenir
const user = await getUserById.execute({ id: 1 });
const anotherUser = await getUserById.execute({ id: 42 });

Bu yaklaşım, özellikle yüksek trafikli API endpoint'lerinde ölçülebilir performans kazanımları sağlar. Veritabanı, sorgu planını bir kez hesaplar ve sonraki çağrılarda yeniden kullanır.


Transaction Desteği

await db.transaction(async (tx) => {
  const [newUser] = await tx
    .insert(users)
    .values({ name: "Yeni Kullanıcı", email: "yeni@example.com" })
    .returning();

  await tx.insert(posts).values({
    title: "Hoş Geldiniz!",
    content: "İlk yazınız otomatik oluşturuldu.",
    authorId: newUser.id,
  });

  // Hata fırlatırsanız transaction otomatik geri alınır
});

Nested transaction'lar (savepoint) da desteklenmektedir.


Zod Entegrasyonu ile Validasyon

Drizzle, drizzle-zod paketi ile şemadan otomatik Zod validasyon şemaları türetebilir:

npm install drizzle-zod
import { createInsertSchema, createSelectSchema } from "drizzle-zod";

const insertUserSchema = createInsertSchema(users, {
  email: (schema) => schema.email("Geçerli bir email adresi giriniz"),
  name: (schema) => schema.min(2, "İsim en az 2 karakter olmalıdır"),
});

const selectUserSchema = createSelectSchema(users);

// API endpoint'inde kullanım
type NewUser = typeof insertUserSchema._type;

function validateUser(data: unknown) {
  return insertUserSchema.parse(data);
}

Bu entegrasyon, şema tanımınızı tek bir kaynak olarak kullanmanızı sağlar (Single Source of Truth). Veritabanı şeması değiştiğinde validasyon kuralları da otomatik olarak güncellenir.


Sonuç

Drizzle ORM, TypeScript ekosistemindeki ORM tartışmasına taze ve güçlü bir bakış açısı getiriyor. "SQL'i soyutlama, onu TypeScript ile güçlendir" felsefesi, özellikle SQL bilgisi olan geliştiriciler için son derece cazip bir yaklaşım.

Güçlü yönleri özetlemek gerekirse:

Dikkat edilmesi gereken noktalar:

Peki, Drizzle gerçekten TypeScript'in en iyi ORM'i mi? Tek bir doğru cevap yok. Eğer SQL'e hakimseniz, performans ve bundle boyutu sizin için önemliyse ve özellikle serverless mimarilerde çalışıyorsanız, Drizzle şu anda en güçlü aday. Ancak hızlı prototipleme, geniş ekosistem ve daha düşük giriş bariyeri arıyorsanız Prisma hâlâ çok geçerli bir seçenek.

Benim tavsiyem: Bir sonraki projenizde Drizzle'ı deneyin. SQL bilginizi TypeScript ile birleştirmenin ne kadar doğal hissettirdiğini gördüğünüzde, muhtemelen geri dönmek istemeyeceksiniz.


Share this post on:

Sonraki Yazı
Prisma ORM vs Drizzle ORM: TypeScript Veritabanı Araçlarının Kapsamlı Karşılaştırması