Aynı ürünü birden fazla kuruma verdiğin anda cevaplaman gereken bir soru çıkıyor: bu kurumların verisi nerede duracak?
Soru ilk bakışta bir veritabanı tercihi gibi görünüyor. Değil. Verdiğin cevap; performansı, yetkilendirmeyi, yedeklemeyi, silme taleplerini ve en çok da yeni sürüm çıkarma biçimini belirliyor. Yanlış tarafı seçmenin bedeli ilk gün değil, altıncı ayda ödeniyor.
Üç yaklaşım
Pratikte üç seçenek var:
Ortak tablo. Bütün kurumlar aynı tabloları paylaşıyor, her satırda bir tenant_id duruyor. Kurulumu en basit olan, işletmesi en ucuz olan yaklaşım.
Ayrı şema. Aynı veritabanı sunucusu, kurum başına ayrı şema. Tablolar çoğalıyor ama veri sınırı netleşiyor.
Ayrı veritabanı. Her kurumun kendi veritabanı var. Uygulama, isteğin hangi kuruma ait olduğunu belirleyip bağlantıyı ona göre seçiyor.
| Ortak tablo | Ayrı şema | Ayrı veritabanı | |
|---|---|---|---|
| Kurulum maliyeti | Düşük | Orta | Yüksek |
| Sızma riski | Yüksek | Orta | Düşük |
| Gürültülü komşu etkisi | Yüksek | Orta | Düşük |
| Kiracı bazlı yedek/geri yükleme | Zor | Orta | Kolay |
| Şema değişikliği | Tek işlem | N işlem | N işlem |
| Kiracılar arası rapor | Kolay | Orta | Zor |
Tablodaki son iki satır kararın gerçek eksenini gösteriyor: izolasyon kazandıkça operasyon zorlaşıyor.
Ortak tablonun asıl riski bir WHERE
Ortak tablo yaklaşımında veri sınırı fiziksel değil, mantıksal. Yani her sorgunun doğru filtreyi taşımasına bağlı:
SELECT * FROM appointments
WHERE tenant_id = :tenant AND status = 'pending';
Bu satır yüzlerce yerde tekrar ediyor ve bir kez unutulması yeterli. Unutulduğunda hata da vermiyor: sorgu çalışıyor, sonuç dönüyor, sadece başka bir kurumun verisi de içinde geliyor.
Bu riski azaltmanın yolları var — global scope'lar, repository katmanı, satır düzeyi güvenlik. Hepsi işe yarıyor. Ama hepsinin ortak özelliği şu: doğru davranış, geliştiricinin bir şeyi hatırlamasına bağlı kalıyor. Sağlık ya da finans verisi taşıyan bir üründe bu, tek başına ayrı veritabanını düşünmek için yeterli bir sebep.
Neden ayrı veritabanını seçtim
Muayenehaneler ve küçük tıp merkezleri için geliştirdiğim bilgi yönetim sisteminde kurum bazlı ayrı veritabanı kullandım. Üç gerekçesi vardı:
- Performans izolasyonu. Büyük bir kurumun veri hacmi, küçük bir kurumun sorgularını yavaşlatmıyor. Ortak tabloda bir kiracının büyümesi herkesin sorun yaşaması demek.
- Veri sınırı. Kurum verisi bir filtreye değil, ayrı bir veritabanı sınırına dayanıyor. Yanlış yazılmış tek bir sorgu diğer kurumu göremiyor.
- Yetki ayrımı. Erişim veritabanı düzeyinde de kısıtlanabiliyor; uygulama katmanının doğru davranmasına ek bir kat daha ekleniyor.
Buna bir de şu eklendi: aynı altyapı farklı ülkelerdeki kuruluşlara hizmet verecekti. Verinin nerede durduğunu kurum kurum söyleyebilmek, sonradan eklenmesi çok zor bir özellik.
Bedeli: şema değişikliği artık bir dağıtım problemi
Ayrı veritabanının maliyeti sorgularda değil. Sorgular neredeyse aynı kalıyor; sadece hangi bağlantıya gittikleri değişiyor.
Asıl mesele şu: tek bir migrasyon, N kez çalışan bir operasyona dönüşüyor. Ve bu operasyon kısmen başarısız olabiliyor. Otuz kurumdan yirmi sekizinde çalışıp ikisinde hata veren bir migrasyon, sistemi tutarsız bir duruma bırakıyor.
Buradan çıkan sorun, bu mimarinin en sinsi tarafı: şema kayması. Tek bir kurumda elle yapılmış bir düzeltme, orada kalıyor. Bir sonraki migrasyon o kurumda farklı bir başlangıç noktası buluyor ve zamanla birbirinden ayrışan kopyalar oluşuyor. Altı ay sonra "bu kurumda neden çalışmıyor" sorusunun cevabı, kodda değil o veritabanının geçmişinde saklı oluyor.
Migrasyonu izlenebilir hâle getirmek
Ayrı veritabanı mimarisinde migrasyon çalıştırmak bir komut değil, bir süreç. Ele alınması gereken dört şey var:
Her kiracının kendi sürümünü bilmek. Hangi migrasyonun nerede uygulandığı, tahmin edilecek değil sorgulanacak bir bilgi olmalı:
SELECT tenant_id, MAX(migration) AS son_surum, MAX(applied_at) AS son_tarih
FROM tenant_migrations
GROUP BY tenant_id
ORDER BY son_surum ASC;
Bu sorgunun sonucunda birden fazla farklı son_surum görüyorsan, kayma başlamış demektir.
Sıralı ve tekrarlanabilir uygulama. Migrasyonlar her kiracıda aynı sırayla çalışmalı ve ikinci kez çalıştırıldığında bozmamalı.
Başarısızlıkta durmak, devam etmemek. Bir kiracıda hata alındığında kalanlara devam etmek, tutarsızlığı büyütüyor. Durup raporlamak daha iyi.
Sonuç raporu. Kaç kiracıda çalıştı, hangilerinde çalışmadı, hata neydi. Bu rapor olmadan "migrasyon tamam" cümlesi bir varsayım.
Yeni kiracı açmak da aynı sürecin parçası. Yeni veritabanı, en güncel şemayla ve tüm migrasyon kayıtları işaretlenmiş hâlde oluşturulmalı — yoksa ilk günden farklı bir noktada başlıyor.
Bağlantı seçimi tek bir yerde kalmalı
Uygulama tarafında dikkat edilecek şey, bağlantı seçiminin dağılmaması. Hangi kurumun veritabanına bağlanılacağı isteğin başında bir kez belirlenmeli; sonrasındaki kod bunu bilmemeli.
// İsteğin başında bir kez: bundan sonrası hangi kiracıda olduğunu bilmez.
$tenant = $this->tenants->resolve($request);
$this->connection->useDatabase($tenant->database);
Bu ayrım korunmadığında, bağlantı seçimi servislere sızıyor ve test edilmesi zor bir yapı ortaya çıkıyor. Daha kötüsü, arka planda çalışan işlerde kiracı bağlamının hiç kurulmaması gibi hatalar mümkün hâle geliyor — kuyruk işçileri bir HTTP isteğinden gelmediği için bu bağlamı ayrıca taşımaları gerekiyor.
Ne zaman hangisi
Karar verirken baktığım şey:
- Veri hassasiyeti yüksekse (sağlık, finans, kişisel veri) izolasyon tarafına yaslan.
- Kiracı sayısı çok yüksek ve her biri küçükse ortak tablo daha yönetilebilir; binlerce veritabanına migrasyon çalıştırmak kendi başına bir problem.
- Kiracı sayısı sınırlı ve her biri büyükse ayrı veritabanı hem performans hem sınır olarak kazandırıyor.
- Kiracılar arası raporlama ürünün merkezindeyse ayrı veritabanı bunu pahalı hâle getiriyor; ayrı bir raporlama katmanı gerekiyor.
- Kiracı bazlı yedek alma veya veri silme talebi bekliyorsan ayrı veritabanı bunu önemsiz bir işe indiriyor.
Son madde giderek daha belirleyici oluyor. "Bu kurumun tüm verisini silin" talebi, ortak tabloda dikkatli bir operasyon; ayrı veritabanında tek bir işlem.
