PostgreSQL ile ORM'yi dağıtın: Tam Kılavuz
Drizzle ORM, TypeScript veritabanı ekosisteminde benzersiz bir konuma sahiptir: okuyabileceğiniz SQL'i üreten kod olarak şema ve veritabanınıza gerçeğin kaynağı gibi davranan bir geçiş sistemi ile aslında yalnızca kılık değiştirmiş bir sorgu oluşturucu olan ORM'dir. Yıllar süren Prisma ve TypeORM'den sonra Drizzle, tam TypeScript çıkarımı ve sıfır çalışma süresi yüküyle ham SQL'e dönüyormuş gibi hissediyor.
Bu kılavuz, ilk kurulumdan üretim modellerine, 65'ten fazla Drizzle şema dosyası içeren bir kod tabanından çizim yapmaya, çok kiracılı sorgulara ve hem Next.js hem de NestJS bağlamlarında güvenilir bir şekilde çalışan tembel bir proxy bağlantı modeline kadar her şeyi kapsar.
Önemli Çıkarımlar
- Veritabanı bağlantısı için tembel bir Proxy kullanın — modül yükleme sırasında asla istekli bağlantılar oluşturmayın
- Yerel PostgreSQL, sistem kurulumlarıyla çakışmaları önlemek için varsayılan olmayan bir bağlantı noktasında (5433) çalışmalıdır
- Çoklu kiracılık için tüm tabloların
organizationIdolması gerekir — şema düzeyinde uygulayın- Drizzle numaralandırma karşılaştırmaları, tür daraltma için açık TypeScript dökümüne ihtiyaç duyar
- Hiçbir zaman
sql.raw()kullanmayın — her zaman parametreli değerlerlesqlşablon değişmez değerini kullanın- İlişkiler yabancı anahtarlardan ayrıdır — tam tip güvenlik için her ikisini de tanımlayın
- İşlemler çok adımlı işlemleri tamamlar;
txnesnesini hizmetlere iletin- Şema dizini (
schema/index.ts) geçişlerin çalışması için tüm tabloları dışa aktarmalıdır
Kurulum ve Kurulum
Drizzle iki paket gerektirir: çekirdek ORM ve veritabanı sürücüsü.
pnpm add drizzle-orm pg
pnpm add -D drizzle-kit @types/pg
Geçişler ve stüdyo dahil tüm yığın için:
pnpm add drizzle-orm postgres
pnpm add -D drizzle-kit
postgres paketi (pg'den farklı), modern TypeScript projelerinde Drizzle için önerilen sürücüdür; bağlantı havuzu oluşturmayı, hazırlanmış ifadeleri destekler ve daha iyi TypeScript türlerine sahiptir.
Tembel Proxy Modeli
Next.js veya NestJS monorepo'sunda Drizzle ile ilgili en kritik mimari karar asla istekli bir veritabanı bağlantısı oluşturmamaktır. Modül yükleme sırasındaki istekli bağlantılar aşağıdaki sorunlara neden olur:
- Next.js: modüller, hiçbir DB'nin bulunmadığı derleme işlemi sırasında içe aktarılır
- NestJS: paket içe aktarmaları ortam değişkenleri yüklenmeden önce gerçekleşir
- Sunucusuz: bağlantı ek yükü nedeniyle soğuk başlatmalar gecikir
Tembel Proxy modeli bunların hepsini çözer:
// packages/db/src/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
let _db: ReturnType<typeof drizzle> | null = null;
function getDb() {
if (!_db) {
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
throw new Error('DATABASE_URL environment variable is not set');
}
const client = postgres(connectionString, {
max: 10, // connection pool size
idle_timeout: 20,
connect_timeout: 10,
});
_db = drizzle(client, { schema });
}
return _db;
}
// Export a Proxy that initializes the connection on first use
export const db = new Proxy({} as ReturnType<typeof drizzle>, {
get(_, prop) {
return getDb()[prop as keyof ReturnType<typeof drizzle>];
},
});
export * from './schema';
Bu kalıp, @ecosire/db içe aktarmanın hiçbir zaman bir veritabanı bağlantısını açmadığı anlamına gelir. Bağlantı yalnızca ilk sorgu çalıştırıldığında oluşturulur.
Şema Tasarımı
Drizzle şemaları, tablo tanımlarını dışa aktaran düz TypeScript dosyalarıdır. Buradaki disiplin, ilgili tabloları aynı dosyada tutmak ve her şeyi merkezi bir dizin aracılığıyla dışa aktarmaktır.
// packages/db/src/schema/contacts.ts
import {
pgTable,
uuid,
text,
varchar,
timestamp,
pgEnum,
boolean,
index,
} from 'drizzle-orm/pg-core';
export const contactTypeEnum = pgEnum('contact_type', ['individual', 'company', 'partner']);
export const contacts = pgTable(
'contacts',
{
id: uuid('id').defaultRandom().primaryKey(),
organizationId: uuid('organization_id').notNull(), // Multi-tenancy
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }),
phone: varchar('phone', { length: 50 }),
type: contactTypeEnum('type').default('individual').notNull(),
isActive: boolean('is_active').default(true).notNull(),
notes: text('notes'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
},
(table) => ({
organizationIdx: index('contacts_organization_idx').on(table.organizationId),
emailIdx: index('contacts_email_idx').on(table.email),
})
);
export type Contact = typeof contacts.$inferSelect;
export type NewContact = typeof contacts.$inferInsert;
$inferSelect ve $inferInsert türleri size tam TypeScript çıkarımı sağlar — Drizzle, şema kısıtlamalarınıza göre ekleme sırasında hangi alanların gerekli, hangilerinin isteğe bağlı olduğunu bilir.
// packages/db/src/schema/index.ts
export * from './contacts';
export * from './orders';
export * from './licenses';
export * from './products';
// ... all tables
Dizin dosyasının, geçiş oluşturma sırasında alabilmesi için drizzle-kit için her tabloyu dışa aktarması gerekir.
İlişkiler
Drizzle, yabancı anahtar kısıtlamalarını (PostgreSQL tarafından uygulanan) ilişki tanımlarından (Drizzle'ın ilişkisel sorgu API'si tarafından kullanılan) ayırır. Her ikisine de ihtiyacınız var:
// packages/db/src/schema/relations.ts
import { relations } from 'drizzle-orm';
import { contacts } from './contacts';
import { orders } from './orders';
import { orderItems } from './order-items';
import { products } from './products';
export const contactsRelations = relations(contacts, ({ many }) => ({
orders: many(orders),
}));
export const ordersRelations = relations(orders, ({ one, many }) => ({
contact: one(contacts, {
fields: [orders.contactId],
references: [contacts.id],
}),
items: many(orderItems),
}));
export const orderItemsRelations = relations(orderItems, ({ one }) => ({
order: one(orders, {
fields: [orderItems.orderId],
references: [orders.id],
}),
product: one(products, {
fields: [orderItems.productId],
references: [products.id],
}),
}));
İlişkiler tanımlandığında Drizzle'ın ilişkisel sorgu API'sini kullanabilirsiniz:
// Fetch order with contact and items in one query
const order = await db.query.orders.findFirst({
where: eq(orders.id, orderId),
with: {
contact: true,
items: {
with: {
product: true,
},
},
},
});
Drizzle tek bir JOIN sorgusu oluşturur; N+1 sorunu yoktur.
Tür Güvenli Sorgular
Drizzle'ın sorgu API'si tamamen tür açısından güvenlidir. Seçim türü, eklediğiniz sütunlara göre daraltılır:
import { db } from '@ecosire/db';
import { contacts } from '@ecosire/db/schema';
import { eq, and, like, sql, count } from 'drizzle-orm';
// Full select — returns Contact[]
const allContacts = await db
.select()
.from(contacts)
.where(eq(contacts.organizationId, orgId))
.limit(50);
// Partial select — returns only specified columns
const contactNames = await db
.select({
id: contacts.id,
name: contacts.name,
email: contacts.email,
})
.from(contacts)
.where(
and(
eq(contacts.organizationId, orgId),
eq(contacts.isActive, true)
)
);
// Count query
const [{ total }] = await db
.select({ total: count() })
.from(contacts)
.where(eq(contacts.organizationId, orgId));
// Search with ILIKE (case-insensitive)
const searchResults = await db
.select()
.from(contacts)
.where(
and(
eq(contacts.organizationId, orgId),
like(contacts.name, `%${searchTerm}%`)
)
)
.limit(20);
Dinamik Where cümleleri (kullanıcı arayüzünden filtreler) için koşulları koşullu olarak oluşturun:
import { SQL } from 'drizzle-orm';
async function searchContacts(orgId: string, filters: {
search?: string;
type?: 'individual' | 'company';
isActive?: boolean;
}) {
const conditions: SQL[] = [eq(contacts.organizationId, orgId)];
if (filters.search) {
conditions.push(like(contacts.name, `%${filters.search}%`));
}
if (filters.type) {
// Enum comparisons need explicit casting
conditions.push(eq(contacts.type, filters.type as 'individual' | 'company' | 'partner'));
}
if (filters.isActive !== undefined) {
conditions.push(eq(contacts.isActive, filters.isActive));
}
return db
.select()
.from(contacts)
.where(and(...conditions))
.limit(100);
}
Enum değerlerindeki açık dönüşüme dikkat edin: filters.type as 'individual' | 'company' | 'partner'. Drizzle'ın numaralandırma türü, dönüşüm olmadan daha dar dize alt türlerini otomatik olarak kabul etmez.
Ekle, Güncelle ve Sil
import { db } from '@ecosire/db';
import { contacts, NewContact } from '@ecosire/db/schema';
import { eq, and } from 'drizzle-orm';
// Insert with RETURNING
async function createContact(data: NewContact) {
const [created] = await db
.insert(contacts)
.values(data)
.returning();
return created; // Fully typed as Contact
}
// Upsert (insert or update on conflict)
async function upsertContact(data: NewContact) {
const [result] = await db
.insert(contacts)
.values(data)
.onConflictDoUpdate({
target: [contacts.organizationId, contacts.email],
set: {
name: data.name,
updatedAt: new Date(),
},
})
.returning();
return result;
}
// Update with RETURNING
async function updateContact(
id: string,
orgId: string,
updates: Partial<NewContact>
) {
const [updated] = await db
.update(contacts)
.set({ ...updates, updatedAt: new Date() })
.where(
and(
eq(contacts.id, id),
eq(contacts.organizationId, orgId) // Always filter by org
)
)
.returning();
return updated;
}
// Soft delete (recommended over hard delete)
async function deleteContact(id: string, orgId: string) {
return db
.update(contacts)
.set({ isActive: false, updatedAt: new Date() })
.where(
and(
eq(contacts.id, id),
eq(contacts.organizationId, orgId)
)
);
}
INSERT ve UPDATE ifadelerinde her zaman .returning() kullanın; bu, oluşturulan/güncellenen kaydı almak için ikinci bir SELECT gidiş dönüşünü önler.
İşlemler
Birlikte başarılı veya başarısız olması gereken çok adımlı operasyonlar işlemlere ihtiyaç duyar:
async function createOrderWithItems(
orderData: NewOrder,
items: NewOrderItem[]
) {
return db.transaction(async (tx) => {
// Create the order
const [order] = await tx
.insert(orders)
.values(orderData)
.returning();
// Create order items
const createdItems = await tx
.insert(orderItems)
.values(items.map((item) => ({ ...item, orderId: order.id })))
.returning();
// Update inventory (must succeed or entire transaction rolls back)
for (const item of items) {
await tx
.update(products)
.set({
stock: sql`${products.stock} - ${item.quantity}`,
})
.where(eq(products.id, item.productId));
}
return { order, items: createdItems };
});
}
İşlem bağlamını hizmet yöntemlerine aktarırken:
// Service accepts optional transaction for composability
async function activateLicense(
licenseId: string,
tx?: typeof db | Parameters<Parameters<typeof db.transaction>[0]>[0]
) {
const queryRunner = tx || db;
return queryRunner
.update(licenses)
.set({ status: 'active', activatedAt: new Date() })
.where(eq(licenses.id, licenseId))
.returning();
}
Çiseleme kitiyle geçişler
drizzle-kit CLI, şema değişikliklerinden geçişler oluşturur:
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/schema/index.ts',
out: './src/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
verbose: true,
strict: true,
});
İş akışı:
# Generate migration SQL from schema changes
npx drizzle-kit generate
# Apply migrations to database
npx drizzle-kit migrate
# Push schema directly (dev only — skips migration files)
npx drizzle-kit push
# Open Drizzle Studio (visual database browser)
npx drizzle-kit studio
Üretim için her zaman migrate kullanın (push değil). push komutu yerel geliştirme yinelemesi içindir; geçiş dosyaları oluşturmaz, dolayısıyla değişiklikler izlenmez.
Sayfalandırma Deseni
Verimli sayfalandırma, hem veri sorgusunu hem de sayım sorgusunu gerektirir:
interface PaginationParams {
page: number;
pageSize: number;
}
async function getContactsPaginated(orgId: string, { page, pageSize }: PaginationParams) {
const offset = (page - 1) * pageSize;
const [data, [{ total }]] = await Promise.all([
db
.select()
.from(contacts)
.where(eq(contacts.organizationId, orgId))
.limit(pageSize)
.offset(offset)
.orderBy(contacts.createdAt),
db
.select({ total: count() })
.from(contacts)
.where(eq(contacts.organizationId, orgId)),
]);
return {
data,
pagination: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
};
}
Her iki sorguyu da Promise.all ile paralel olarak çalıştırmak, sıralı yürütmeye kıyasla yanıt süresini yarı yarıya azaltır.
Yaygın Tuzaklar ve Çözümler
Tuzak 1: Dinamik sorgular için sql.raw()'ın kullanılması
// Dangerous — SQL injection vulnerability
const results = await db.execute(
sql.raw(`SELECT * FROM contacts WHERE name = '${userInput}'`)
);
// Safe — parameterized
const results = await db.execute(
sql`SELECT * FROM contacts WHERE name = ${userInput}`
);
2. Tuzak: Sık sorgulanan sütunlarda eksik dizinler
Filtrelediğiniz sütunlara her zaman dizinler ekleyin. En sık kaçırılan indeksler:
(table) => ({
organizationIdx: index('contacts_org_idx').on(table.organizationId),
emailIdx: index('contacts_email_idx').on(table.email),
compositeIdx: index('contacts_org_active_idx').on(
table.organizationId,
table.isActive
),
})
Tuzak 3: Manuel ilişki yüklemesinden N+1 sorgu
// N+1 problem — one query per order
const orders = await db.select().from(ordersTable);
for (const order of orders) {
order.contact = await db
.select()
.from(contacts)
.where(eq(contacts.id, order.contactId));
}
// Solution — use Drizzle's relational query API
const orders = await db.query.orders.findMany({
with: { contact: true },
});
4. Tuzak: schema/index.ts'den yeni tabloları dışa aktarmayı unutmak
Yeni bir şema dosyası oluşturur ancak schema/index.ts'dan yeniden dışarı aktarmayı unutursanız, drizzle-kit yeni tabloyu görmez ve geçişler oluşturulmaz.
Sıkça Sorulan Sorular
TypeScript projeleri için Drizzle Prisma ile nasıl karşılaştırılır?
Drizzle, TypeScript türleriyle ham SQL'e daha yakındır; SQL ile 1:1 eşlenen sorgular yazarsınız. Prisma, özel bir sorgu dili ve ayrı bir şema dosyasıyla daha fazlasını özetler. Drizzle, karmaşık sorgularda daha iyi TypeScript çıkarımına, daha küçük çalışma zamanı yüküne sahiptir ve kod oluşturma adımı gerektirmez. Prisma daha büyük bir ekosisteme ve geçici silme ve denetim günlüğü tutma gibi daha yerleşik özelliklere sahiptir. SQL konusunda rahat olan büyük ekipler için genellikle Drizzle tercih edilir.
CI/CD'de drizzle-kit push'u mu yoksa taşımayı mı kullanmalıyım?
CI/CD'de her zaman drizzle-kit migrate kullanın. push komutu hızlı yerel geliştirme içindir; geçiş dosyaları oluşturmadan şema değişikliklerini doğrudan uygular, dolayısıyla denetim izi yoktur ve geri alma zordur. Dağıtım hattınızda, şemanın senkronize olduğundan emin olmak için uygulamayı başlatmadan önce drizzle-kit migrate komutunu çalıştırın.
Drizzle ile veritabanı tohumlamayı nasıl hallederim?
Ayrı bir çekirdek dosyası oluşturun ve şemanızı doğrudan içe aktarın. Yeniden çalıştırmada başarısız olmayacak idempotent tohumlar için onConflictDoNothing() kullanın. Kolay güncellemeler için tohum verilerini sabitlerde saklayın ve geliştirme kurulum komut dosyanızda geçişlerden sonra tohumları çalıştırın.
Drizzle ve PostgreSQL ile tam metin aramasını nasıl gerçekleştiririm?
sql şablon değişmez değerini PostgreSQL'in to_tsvector ve to_tsquery işlevleriyle birlikte kullanın. Drizzle tam metin aramasını soyutlamaz, ancak doğrudan yazabilirsiniz. Performans için tsvector sütununa bir GIN dizini ekleyin ve doğal dil sorgularını güvenli bir şekilde ayrıştırmak amacıyla kullanıcı tarafından sağlanan arama terimleri için websearch_to_tsquery kullanın.
Şema değişikliklerini kesinti olmadan nasıl halledebilirim?
Eklemeli geçişleri kullanın; önce sütunları null olarak ekleyin, verileri doldurun, ardından NOT NULL kısıtlamasını ekleyin. Sütunları koddan kaldıran aynı taşıma işlemine asla bırakmayın. Sıra şu şekildedir: 1) Eski sütunu yok sayan kodu dağıtın, 2) Taşıma işlemini bırakarak onu bırakın. Bu, veri kaybı olmadan geri almanın her zaman mümkün olmasını sağlar.
Sonraki Adımlar
İyi tasarlanmış bir veritabanı katmanı, her üretim uygulamasının üzerine inşa edildiği temeldir. ECOSIRE'ın mühendislik ekibi her gün Drizzle ORM ve PostgreSQL ile çalışarak karmaşık, çok kiracılı bir sistemde 65'ten fazla şema dosyasını yönetir.
İster veritabanı mimarisi danışmanlığına, Odoo ERP entegrasyonuna, ister modern TypeScript araçlarıyla oluşturulmuş eksiksiz bir arka uç sistemine ihtiyacınız olsun, nasıl yardımcı olabileceğimizi öğrenmek için hizmetlerimizi keşfedin.
Yazan
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
İlgili Makaleler
Zero-Downtime Database Migrations with Drizzle ORM
Run database migrations without downtime using Drizzle ORM. Covers expand-contract pattern, backward-compatible schema changes, rollback strategies, and CI/CD integration for PostgreSQL.
Odoo Performance Tuning: PostgreSQL and Server Optimization
Expert guide to Odoo 19 performance tuning. Covers PostgreSQL configuration, indexing, query optimization, Nginx caching, and server sizing for enterprise deployments.
Natural Language Database Queries with OpenClaw
How OpenClaw enables natural language database queries, translating plain English business questions into accurate SQL without exposing database credentials or query complexity.