Ben Enes Şahin, 2022'den beri üretimde çalışan bir e-ticaret backend'inde Prisma ve PostgreSQL kullanıyorum. Bu yazıda bir şemayı sıfırdan tasarlarken önemli gördüğüm kararları, örnek bir sipariş şemasıyla anlatıyorum.
Neden Prisma
Prisma, veritabanı şemasını tek bir dosyada (schema.prisma) tanımlamanı, oradan hem migration'ları hem de tip güvenli bir istemci (client) üretmeni sağlayan bir ORM. Ham SQL yazmaya göre avantajı, şemadaki bir alanı değiştirdiğinde TypeScript derleyicisinin onu kullanan her yeri anında sana göstermesi; bir ilişkiyi yanlış kurarsan bunu üretimde değil, editörde fark ediyorsun.
Modelleri ilişkilerle kurmak
Bir sipariş sisteminde kullanıcı, sipariş ve sipariş kalemi arasındaki ilişki şöyle görünüyor:
model User {
id String @id @default(cuid())
email String @unique
role Role @default(CUSTOMER)
orders Order[]
}
model Order {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
status OrderStatus @default(PENDING)
items OrderItem[]
totalCents Int
createdAt DateTime @default(now())
@@index([userId])
}
model OrderItem {
id String @id @default(cuid())
orderId String
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
productId String
quantity Int
priceCents Int
}
enum Role {
CUSTOMER
ADMIN
}
enum OrderStatus {
PENDING
PAID
SHIPPED
CANCELLED
}Burada birkaç kararın altını çizmek istiyorum:
- Parayı
Int(cent cinsinden) tutmak,Floatkullanmamak gerekir. Kayan noktalı sayılar parasal hesaplamalarda yuvarlama hatası yapabiliyor; 10.10 TL gibi bir tutarFloatile bazen 10.099999999 olarak saklanabiliyor. Cent'e çevirip tam sayı olarak tutmak bu sınıf hatayı tamamen ortadan kaldırıyor. OrderItem,OrdersilininceonDelete: Cascadeile otomatik siliniyor, amaOrder,Usersilinince silinmiyor (cascade yazmadım). Bir siparişin kalemleri sipariş olmadan anlamsız, ama bir kullanıcı silinse bile sipariş geçmişinin durması gerekiyor; muhasebe ve iade süreçleri buna bağlı.@@index([userId]): "kullanıcının siparişlerini getir" bir sipariş sisteminde en sık çalışan sorgulardan biri, bu yüzdenuserIdüzerinde açık bir indeks tanımlamak mantıklı. Prisma foreign key için otomatik indeks eklemiyor, bunu elle yazman gerekiyor.
Migration akışı
Şemada değişiklik yaptığında:
npx prisma migrate dev --name add_order_statusBu komut hem yeni bir SQL migration dosyası üretiyor hem de yerel veritabanına uyguluyor. Migration dosyalarını git'e commit etmek gerekir; production'a çıkarken prisma migrate deploy ile aynı dosyalar sırayla uygulanıyor. Böylece yerel şema ile production'daki şema arasında fark kalmıyor, "bende çalışıyordu" durumları migration seviyesinde ortadan kalkıyor.
N+1 sorgu tuzağı
Prisma'da ilişkili veriyi çekerken en sık yapılan hata, döngü içinde sorgu atmak:
// Kötü: her sipariş için ayrı bir sorgu atıyor (N+1)
const orders = await prisma.order.findMany();
for (const order of orders) {
order.items = await prisma.orderItem.findMany({ where: { orderId: order.id } });
}Bunun yerine include ile ilişkiyi tek sorguda getiriyorum:
// İyi: tek sorguda, join ile
const orders = await prisma.order.findMany({
include: { items: true },
});100 siparişlik bir listede ilki 101 sorgu atarken ikincisi tek sorgu atıyor. Küçük veri setlerinde fark görünmüyor, ama liste büyüdükçe ilk yaklaşım gerçek bir performans sorununa dönüşüyor.
Transaction ile veri bütünlüğü
Bir sipariş oluştururken hem Order kaydı açılıyor hem stok düşülüyor. Bu iki işlemden biri başarılı olup diğeri başarısız olursa (örneğin stok yetersizse) veritabanı tutarsız bir duruma düşer. Bunu $transaction ile engellemek gerekir:
await prisma.$transaction(async (tx) => {
const product = await tx.product.findUniqueOrThrow({ where: { id: productId } });
if (product.stock < quantity) throw new Error("Stok yetersiz");
await tx.product.update({
where: { id: productId },
data: { stock: { decrement: quantity } },
});
await tx.order.create({
data: { userId, totalCents, items: { create: [{ productId, quantity, priceCents }] } },
});
});Transaction içindeki herhangi bir adım hata fırlatırsa, o ana kadar yapılan tüm değişiklikler geri alınıyor. Stok düşürülüp sipariş oluşturulamadan yarıda kalmış bir kayıt riski böylece ortadan kalkıyor.
Sonuç
Bir Prisma şeması tasarlarken önemli gördüğüm dört karar: parasal alanları tam sayı olarak tutmak, cascade kurallarını verinin gerçek yaşam döngüsüne göre seçmek, sık sorgulanan alanlara indeks eklemeyi unutmamak ve birden fazla adımı olan işlemleri transaction'a almak. Bunların hiçbiri Prisma'ya özgü değil, ama Prisma bu kararları şemada açıkça görünür kılıyor.
Sık sorulan sorular
Parayı veritabanında neden Float yerine Int tutmalıyım?
Kayan noktalı sayılar yuvarlama hatası yapabilir; 10.10 gibi bir tutar 10.099999999 olarak saklanabilir. Tutarı kuruş (cent) cinsinden tam sayı tutmak bu hatayı ortadan kaldırır.
Prisma foreign key için otomatik indeks oluşturur mu?
Hayır, Prisma foreign key alanları için otomatik indeks eklemez. Sık sorguladığın alanlara @@index ile elle indeks tanımlaman gerekir.
Prisma'da N+1 sorgu problemi nedir?
Döngü içinde her kayıt için ayrı sorgu atmaktır; 100 kayıt için 101 sorgu çalışır. include ile ilişkili veriyi tek sorguda getirerek önlenir.
Prisma'da $transaction ne zaman kullanılır?
Birden fazla adımı olan ve biri başarısız olursa hepsinin geri alınması gereken işlemlerde, örneğin sipariş oluştururken stok düşmek gibi. Adımlardan biri hata verirse o ana kadar yapılan değişiklikler geri alınır.