Drizzle ORM ile Sıfır Kesinti Süreli Veritabanı Geçişleri
Veritabanı geçişleri, üretim dağıtımlarındaki en tehlikeli işlemdir. Bir tabloyu (200 ms bile olsa) kilitleyen bir geçiş, bağlantı zaman aşımlarına yol açarak 500 hataya ve gece saat 2'de uyarı fırtınalarına neden olur. Çoğu ORM geçiş öğreticisi size "kolay" yolu gösterir: uygulamayı kapatın, geçişi çalıştırın, yeniden başlatın. Bu sıfır kesinti süresi değil; bu planlı bakımdır.
Gerçek sıfır kesinti süreli geçişler farklı bir zihinsel model gerektirir: şemanız, dağıtım süreci boyunca o anda çalışan uygulama koduyla geriye dönük olarak uyumlu olmalıdır. Bu, eski kodun ve yeni kodun kullanıma sunma penceresi sırasında aynı anda çalışması gerektiği anlamına gelir. Genişletme-sözleşme modeli bunu mümkün kılar. Drizzle ORM'nin SQL öncelikli yaklaşımı, bunu doğru şekilde uygulamanız için size kontrol sağlar.
Önemli Çıkarımlar
drizzle-kit pushürününü hiçbir zaman üretimde çalıştırmayın; SQL'i oluşturun ve inceleyin, ardından kontrollü bir şekilde uygulayın- Genişletme-sözleşme modeli: ekleme (genişletme) → yeni kodu dağıtma → eskisini kaldırma (sözleşme)
- Null yapılabilir bir sütun eklemek güvenlidir; NOT NULL'u varsayılan olmadan eklemek tehlikelidir
- Bir sütunu yeniden adlandırmak şunları gerektirir: yeni sütun ekleyin → dolgu → kodu güncelleyin → eski sütunu kaldırın (4 adım)
- Dizin oluşturma sırasında tablo kilitlenmelerini önlemek için PostgreSQL
CREATE INDEX CONCURRENTLYkullanın- Üretime başvurmadan önce veri tabanınızın üretim boyutunda bir kopyası üzerinde geçişleri test edin
- Her zaman bir geri alma planınız olsun: her geçiş için geri alma SQL'sini yazın ve test edin
- Tablonun yeniden yazılmasını gerektiren şema değişiklikleri (büyük bir tabloda ALTER TYPE) bakım aralıkları gerektirir
Drizzle ORM Şeması Kurulumu
// packages/db/src/schema/contacts.ts
import { pgTable, uuid, varchar, text, timestamp, pgEnum, index } from 'drizzle-orm/pg-core';
export const contactStatusEnum = pgEnum('contact_status', ['active', 'inactive', 'archived']);
export const contacts = pgTable('contacts', {
id: uuid('id').primaryKey().defaultRandom(),
organizationId: uuid('organization_id').notNull(),
name: varchar('name', { length: 255 }).notNull(),
email: varchar('email', { length: 255 }),
phone: varchar('phone', { length: 50 }),
status: contactStatusEnum('status').default('active').notNull(),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
}, (table) => ({
orgIdIdx: index('contacts_org_id_idx').on(table.organizationId),
emailIdx: index('contacts_email_idx').on(table.email),
statusIdx: index('contacts_status_idx').on(table.status),
}));
Geçişler oluştur:
# Generate SQL migration (never auto-apply to production)
npx drizzle-kit generate
# Review the generated SQL before applying
cat drizzle/0001_add_contact_status.sql
# Apply to development database
npx drizzle-kit migrate
# For production: apply via your deployment pipeline
Genişletme-Sözleşme Modeli
Temel prensip: Her şema değişikliği, birden fazla geriye dönük uyumlu dağıtıma bölünür.
Aşama 1: Genişletme (Kesmeden Ekle)
Yalnızca eklemeli olan şema değişikliklerini dağıtın (varsayılanlara sahip yeni sütunlar, yeni tablolar, yeni dizinler).
-- Migration: 0010_expand_add_company.sql
-- Safe to apply while old code is running: nullable column with a default
ALTER TABLE contacts
ADD COLUMN IF NOT EXISTS company_id UUID,
ADD COLUMN IF NOT EXISTS company_name VARCHAR(255);
-- Old code ignores these columns; new code uses them
-- Both versions coexist during rolling deployment
// Drizzle schema after expansion
export const contacts = pgTable('contacts', {
// ... existing columns ...
companyId: uuid('company_id'), // nullable — old code ignores it
companyName: varchar('company_name', { length: 255 }), // nullable — safe
});
Aşama 2: Yeni Uygulama Kodunu Dağıtın
Hem eski hem de yeni sütunları okuyup yazan yeni uygulama sürümünü dağıtın. Devamlı dağıtım sırasında, eski örnekler (şirket desteği yok) ve yeni örnekler (şirket desteği) aynı anda çalışır; sütun geçersiz kılınabildiğinden her ikisi de çalışır.
Aşama 3: Dolgu
Tablo kilitlenmelerini önlemek için mevcut satırlar için yeni sütunu küçük gruplar halinde doldurun:
-- Migration: 0011_backfill_company_name.sql
-- Run in small batches to avoid locking
DO $$
DECLARE
batch_size INTEGER := 1000;
offset_val INTEGER := 0;
rows_updated INTEGER;
BEGIN
LOOP
UPDATE contacts
SET company_name = 'Unknown'
WHERE company_name IS NULL
AND id IN (
SELECT id FROM contacts
WHERE company_name IS NULL
ORDER BY id
LIMIT batch_size
OFFSET offset_val
);
GET DIAGNOSTICS rows_updated = ROW_COUNT;
EXIT WHEN rows_updated = 0;
offset_val := offset_val + batch_size;
-- Brief pause between batches to reduce I/O pressure
PERFORM pg_sleep(0.1);
END LOOP;
END $$;
Aşama 4: Sözleşme (Eski Sütunu Kaldır)
Tüm uygulama kodu güncellendikten ve dolgu tamamlandıktan sonra eski sütunu veya kısıtlamayı kaldırın.
Güvenli Geçiş Modelleri
NOT NULL Sütunu Ekleme
-- WRONG — will fail if table has rows (no default, no nullable)
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20) NOT NULL;
-- WRONG — locks table while it writes the default to every row
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20) NOT NULL DEFAULT 'free';
-- CORRECT — three-step approach
-- Step 1: Add nullable column
ALTER TABLE contacts ADD COLUMN tier VARCHAR(20);
-- Step 2: Backfill existing rows
UPDATE contacts SET tier = 'free' WHERE tier IS NULL;
-- Step 3 (next deployment): Add NOT NULL constraint (instant if no NULLs exist)
ALTER TABLE contacts ALTER COLUMN tier SET NOT NULL;
ALTER TABLE contacts ALTER COLUMN tier SET DEFAULT 'free';
Bir Sütunu Yeniden Adlandırma (4 Adımlı İşlem)
-- Step 1: Add the new column
ALTER TABLE contacts ADD COLUMN full_name VARCHAR(255);
-- Step 2: Backfill + keep in sync with a trigger
UPDATE contacts SET full_name = name;
CREATE OR REPLACE FUNCTION sync_full_name() RETURNS trigger AS $$
BEGIN
NEW.full_name := NEW.name;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER contacts_sync_full_name
BEFORE INSERT OR UPDATE OF name ON contacts
FOR EACH ROW EXECUTE FUNCTION sync_full_name();
-- Step 3: Deploy new code that writes to full_name, reads from both
-- Step 4 (next deployment, after all instances updated):
DROP TRIGGER contacts_sync_full_name ON contacts;
DROP FUNCTION sync_full_name;
ALTER TABLE contacts DROP COLUMN name;
Sıralama Türünü Değiştirme
PostgreSQL, tam tablo yeniden yazılmadan enum değerlerinin kaldırılmasına izin vermez. Güvenli numaralandırma değişiklikleri için:
-- Adding a new enum value is safe (instant, no lock)
ALTER TYPE contact_status ADD VALUE IF NOT EXISTS 'pending';
-- Removing an enum value requires a full table rewrite
-- Use the expand-contract pattern: add a new enum type, migrate, drop old
CREATE TYPE contact_status_new AS ENUM ('active', 'inactive', 'archived', 'pending');
-- Migrate data
ALTER TABLE contacts
ALTER COLUMN status TYPE contact_status_new
USING status::text::contact_status_new;
DROP TYPE contact_status;
ALTER TYPE contact_status_new RENAME TO contact_status;
Kilitlemeden Dizin Oluşturma
Normal CREATE INDEX, bu süre boyunca tüm yazma işlemlerini engelleyen bir ShareLock alır. Büyük bir masada bu işlem birkaç dakika sürebilir.
-- WRONG — locks writes during index creation
CREATE INDEX contacts_email_idx ON contacts(email);
-- CORRECT — concurrent index creation; no write lock
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_email_idx
ON contacts(email)
WHERE email IS NOT NULL; -- Partial index for better performance
-- Drizzle note: drizzle-kit does not generate CONCURRENTLY by default
-- Edit the generated SQL migration to add CONCURRENTLY before applying
Drizzle geçiş dosyanıza, oluşturma işleminden sonra CONCURRENTLY öğesini manuel olarak ekleyin:
-- drizzle/0012_add_email_index.sql (edited after generation)
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_email_idx
ON contacts (email)
WHERE email IS NOT NULL;
Üretim CI/CD'sinde Yoğun Geçiş
Üretimde asla drizzle-kit push kullanmayın; geçişleri inceleme adımı olmadan uygular. Sürüm kontrollü oluşturulan SQL dosyalarıyla drizzle-kit migrate kullanın:
# scripts/deploy-production.sh
echo "=== Running database migrations ==="
# Check if there are pending migrations
PENDING=$(npx drizzle-kit migrate --dry-run 2>&1 | grep "pending")
if [ -n "$PENDING" ]; then
echo "Pending migrations detected:"
echo "$PENDING"
# Apply migrations
npx drizzle-kit migrate
echo "Migrations applied successfully"
else
echo "No pending migrations"
fi
// Programmatic migration in main.ts (NestJS)
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
async function runMigrations() {
const migrationClient = postgres(process.env.DATABASE_URL!, { max: 1 });
const db = drizzle(migrationClient);
await migrate(db, {
migrationsFolder: join(__dirname, '..', '..', '..', 'drizzle'),
});
await migrationClient.end();
}
// In bootstrap(), before app.listen():
if (process.env.RUN_MIGRATIONS === 'true') {
await runMigrations();
}
Geri Alma Stratejileri
Her geçişin, dağıtımdan önce yazılıp test edilmiş bir tamamlayıcı geri alma geçişine sahip olması gerekir:
// drizzle/0013_add_company_id.sql (forward migration)
ALTER TABLE contacts ADD COLUMN IF NOT EXISTS company_id UUID;
CREATE INDEX CONCURRENTLY IF NOT EXISTS contacts_company_id_idx
ON contacts(company_id);
// drizzle/rollbacks/0013_rollback_add_company_id.sql
DROP INDEX CONCURRENTLY IF EXISTS contacts_company_id_idx;
ALTER TABLE contacts DROP COLUMN IF EXISTS company_id;
Acil geri alma için:
# Emergency rollback script
# Run the rollback SQL, then redeploy the previous app version
psql "$DATABASE_URL" < drizzle/rollbacks/0013_rollback_add_company_id.sql
git checkout HEAD~1
pnpm build && pm2 restart ecosystem.config.cjs --update-env
Geçiş Test Stratejisi
// packages/db/src/tests/migration.spec.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import postgres from 'postgres';
import { drizzle } from 'drizzle-orm/postgres-js';
describe('Database Migrations', () => {
let sql: ReturnType<typeof postgres>;
beforeAll(async () => {
sql = postgres(process.env.TEST_DATABASE_URL!);
});
it('all migrations apply cleanly on a fresh database', async () => {
const db = drizzle(sql);
await expect(migrate(db, { migrationsFolder: './drizzle' }))
.resolves.not.toThrow();
});
it('migrations are idempotent (can be applied twice safely)', async () => {
const db = drizzle(sql);
await migrate(db, { migrationsFolder: './drizzle' });
// Running again should be a no-op, not an error
await expect(migrate(db, { migrationsFolder: './drizzle' }))
.resolves.not.toThrow();
});
});
Bakım Aralığı Gerektiren Tehlikeli İşlemler
Çok büyük tablolarda (yüz milyonlarca satır) bazı işlemler sıfır kesintiyle yapılamaz:
| Operasyon | Risk | Azaltma |
|---|---|---|
ALTER TYPE USING büyük sütunda | Tam tablonun yeniden yazılması, uzun kilit | Bakım zamanlaması penceresi |
ADD COLUMN NOT NULL varsayılan olmadan | Satırlar mevcutsa anında başarısızlık | Üç adımlı genişletme sözleşmesini kullanın |
CREATE INDEX (AYNI ZAMANDA olmadan) | Süre boyunca yazma kilidi | Her zaman CONCURRENTLY kullanın |
| KOD0 | Anında, ancak eski kodu bozuyor | Yalnızca kod güncellendikten sonra genişletme aşamasında |
| KOD0 | Geri döndürülemez | Her zaman bir yedeğiniz olsun; 30 günlük geçici silme politikası |
| KOD0 | Süre boyunca masa kilidi | Bakım sırasında normal VACUUM + zamanlama FULL'u kullanın |
Sıkça Sorulan Sorular
drizzle-kit push ile drizzle-kit migrate arasındaki fark nedir?
push, geçiş dosyaları oluşturmadan mevcut şemanızı doğrudan veritabanına uygular; geçiş geçmişini umursamadığınız geliştirmeler için kullanışlıdır. migrate, drizzle/ dizininizdeki SQL dosyalarını, uygulanmış olan izlemeyi sırasıyla uygular. Üretimde her zaman migrate kullanın; böylece her şema değişikliğinin kontrollü, gözden geçirilebilir, sürüm kontrollü geçmişine sahip olursunuz.
Drizzle, drizzle_migrations izleme tablosunu nasıl ele alıyor?
Drizzle, hangi geçiş dosyalarının ne zaman uygulandığını kaydeden bir drizzle_migrations (veya __drizzle_migrations) tablosu oluşturur. Geçişi uygulamadan önce bu tabloyu kontrol eder. Bu tablodaki satırları manuel olarak eklemeyin veya silmeyin; bu, Drizzle'ın durum izleme mekanizmasıdır. Önceki bir geçiş durumuna geri dönmeniz gerekiyorsa geri alma SQL'nizi çalıştırın ve ilgili satırı manuel olarak silin.
Geçişleri üretim boyutunda bir veritabanına karşı nasıl test ederim?
Üretim veritabanınızın arındırılmış (PII'si kaldırılmış) bir kopyasını bir hazırlama sunucusuna geri yükleyin. Geçişi buraya uygulayın ve şunları ölçün: duvar saati süresi, kilit bekleme süresi (pg_stat_activity'den), tablo şişkinliği (pg_stat_user_tables'den) ve sorgu performansı üzerindeki etki (EXPLAIN ANALYZE'den). Geçiş işlemi hazırlama sırasında 5 saniyeden uzun sürerse yeniden tasarlayın.
Veritabanı geçişleri için işlemleri kullanabilir miyim?
PostgreSQL'deki DDL'lerin çoğu işlemseldir; ALTER TABLE, CREATE TABLE, CREATE INDEX'yi (fakat CREATE INDEX CONCURRENTLY DEĞİL) bir işleme sarabilir ve herhangi bir adım başarısız olursa geri sarabilirsiniz. Drizzle, varsayılan olarak her geçiş dosyasını bir işlemde sarar. CONCURRENTLY kullanan geçişler için, bunları bir işlemin dışında çalışan ayrı bir geçiş dosyasına bölmeniz gerekir.
TypeScript'te Drizzle numaralandırma karşılaştırmalarını nasıl halledebilirim?
Drizzle'ın numaralandırma sütunları veritabanından dize değerini döndürür. Karşılaştırma sırasında TypeScript türü doğru şekilde daraltamayabilir. Açıkça yayınlayın: if ((contact.status as ContactStatus) === 'active') veya Drizzle where yan tümcenizde bir tür iddiası kullanın: where(eq(contacts.status, 'active' as ContactStatus)). Bu, TypeScript'i mutlu tutarken çalışma zamanı hatalarını da önler.
Sonraki Adımlar
Sıfır kesinti süreli veritabanı geçişleri disiplin gerektirir; ancak alternatif (kesinti süresi, veri bozulması, sabah 3'te acil geri dönüşler) çok daha kötüdür. Genişletme-sözleşme modeli, CREATE INDEX CONCURRENTLY ve uygun geri alma hazırlığı, şema değişikliklerini rutin, güvenli bir işlem haline getirir.
ECOSIRE, Drizzle ORM, PostgreSQL 17 ve 65'ten fazla şema dosyası ve 300'den fazla geçişte test edilen sıfır kesinti süreli dağıtım hattı ile NestJS uygulamalarına yönelik veritabanı şemalarını yönetir. Veritabanı işlemlerini geniş ölçekte güvenli bir şekilde nasıl yürüttüğümüzü öğrenmek için Arka uç mühendislik hizmetlerimizi keşfedin.
Yazan
ECOSIRE Research and Development Team
ECOSIRE'da kurumsal düzeyde dijital ürünler geliştiriyor. Odoo entegrasyonları, e-ticaret otomasyonu ve yapay zeka destekli iş çözümleri hakkında içgörüler paylaşıyor.
İlgili Makaleler
AWS EC2 Deployment Guide for Web Applications
Complete AWS EC2 deployment guide: instance selection, security groups, Node.js deployment, Nginx reverse proxy, SSL, auto-scaling, CloudWatch monitoring, and cost optimization.
Drizzle ORM with PostgreSQL: Complete Guide
Complete guide to Drizzle ORM with PostgreSQL: schema design, migrations, type-safe queries, relations, transactions, and production patterns for TypeScript apps.
Enterprise Security for OpenClaw AI Deployments
Comprehensive guide to securing OpenClaw AI agent deployments in enterprise environments. Covers authentication, secrets management, network isolation, and compliance.