Özet
Özet
- Shared-database çoklu kiracılık = bir veritabanı, her satır
tenant_idile etiketlenir, otomatik olarak filtreleyen bir global scope. - Üç hareketli parça: bir
TenantContextsingletonı, birBelongsToTenanttrait’i ve bileşik benzersizlik kısıtları. - Gerçek güvenlik ağı disiplin değil — bir CI testi, bir
tenant_idtablosunun trait’i unuttuğunda (veya tersine) başarısız olur.
Bu hafta shared-database çoklu kiracılığının temel katmanını gönderdim. Yalnızca “bir satır bir kuruluşa aittir” özelliğini sağlayan kısmı yaptım ve bunu yanlış yapmayı zorlaştırdım. İşte yapısı.
Neden shared-database?
Neden shared-database?
Üç geniş strateji bulunmaktadır. Hızlı bir karşılaştırma:
| Strateji | Yalıtım | Operasyon maliyeti | İyi olduğu durumlar |
|---|---|---|---|
| Database-per-tenant | En güçlü | En yüksek (migrations × N) | Az sayıda büyük kiracı, sıkı uyum |
| Schema-per-tenant | Güçlü | Orta | Postgres, orta kiracı sayısı |
Shared DB + tenant_id | En zayıf | En düşük | Birçok kiracı, tek kod tabanı, tek migration çalıştırma |
Birden fazla kuruluşun tek bir kod tabanını paylaşacağı bir yeşil saha uygulaması için, shared-database sadelik açısından kazanır. Ancak tuzak şu: yalıtım artık sizin işiniz, uygulama kodunda zorunlu hale getirilmiştir. Bir where tenant_id = ? unutur ve bir kuruluş diğerinin verilerini görebilir. Bu yüzden bütün tasarım, bu durumu unutmayı imkansız hale getirmeye yöneliktir.
Bağlam Nesnesi
Bağlam Nesnesi
Her şey mevcut kiracıyı tek bir singleton’dan okur, istekten, oturumdan veya globalden değil. Bu, çözümleyicinin değiştirilebilir olmasını sağlar — konfigürasyon bazlı on-premise, alan bazlı SaaS — aşağıdaki hiçbir şeyi etkilemeden.
class TenantContext
{
private ?Tenant $tenant = null;
private bool $scopeDisabled = false;
public function id(): ?int { return $this->tenant?->getKey(); }
public function has(): bool { return $this->tenant instanceof Tenant; }
public function shouldScope(): bool
{
return ! $this->scopeDisabled && $this->has();
}
public function withoutScope(Closure $callback): mixed
{
// gerçek birbirlerine ait çalışmalarda kapsamı askıya al, ardından geri yükle
}
}
shouldScope()‘un false döndürmesi önemli: konsol başlatma, veritabanı doldurma ve erken testler, bir kiracı olmadan çalıştırılır. Kiracı yoksa filtrelemek için hiçbir şey yok — “null ile filtrele” değil.
Trait
Trait
Bir trait iki işi yapar: okuma sırasında global scope ekler, yazma sırasında tenant_id doldurur.
trait BelongsToTenant
{
public static function bootBelongsToTenant(): void
{
$context = app(TenantContext::class);
static::addGlobalScope('tenant', function (Builder $q) use ($context) {
if (! $context->shouldScope()) return;
$q->where($q->getModel()->getTable().'.tenant_id', $context->id());
});
static::creating(function ($model) use ($context) {
if ($model->tenant_id === null && $context->has()) {
$model->tenant_id = $context->id();
}
});
}
// ...
}
Tabloyu nitelikli kılan sütun (table.tenant_id) iki ilişkili tabloyu birleştirirken belirsiz sütun hatalarını önler.
Benzersizlik Kıvrımları
Benzersizlik Kıvrımları
Shared-database altında, “benzersiz” ikiye ayrılır. Kiracıların yeniden kullanması gereken okunabilir bir tanımlayıcı, her kiracı için benzersiz olmalıdır. Bir güvenlik tokenı global olarak benzersiz kalmalıdır — burada bir çarpışma, bir saldırı yüzeyi değil, bir kullanıcı deneyimi aksaklığıdır.
| Sütun türü | Kapsama | Örnek |
|---|---|---|
| İnsan tanımlayıcıları | unique([tenant_id, x]) | üye numarası, fatura numarası, slug |
| UUID’lar & tokenlar | Global benzersiz | qr token, idempotency anahtarı, gateway referansı |
Kullanıcılar da merkezi bir öneme sahiptir: bir e-posta, bir giriş, birçok orga üye olarak dahil olma. Kişi kiracı kapsamına alınmaz; onların üyelikleri vardır.
Gerçekten Güvenli Olmanızı Sağlayan Kısım
Gerçekten Güvenli Olmanızı Sağlayan Kısım
Traitler ve kısıtlar uygulanmalarını aklınızda tutmak kadar iyidir. Bu yüzden güvenlik şeridi bir testtir, kontrol listesi değil:
it('her tablosu tenant_id olan modeli süzmelidir', function () {
$missing = [];
foreach (tenancyModelClasses() as $class) {
$table = (new $class)->getTable();
if (Schema::hasColumn($table, ) && ! usesBelongsToTenant($class)) {
$missing[] = $class;
}
}
expect($missing)->toBe([]); // CI başarısız olur ve suçluları adlandırır
});
Bu test, hem trait’i unutan bir tenant_id tablosunu hem de sütunu unutan bir trait’i kontrol eder. Altı ay sonra yeni bir model ekleyin, trait’i unutun ve CI, incelemeden önce size haber verir.
Sonuç
Sonuç
Shared-database kiracılığı işletmesi ucuz ve sızdırması kolaydır. Çözüm, dikkatli olmak değil — “Bunu hatırladık mı?” sorusunu bir başarısız teste dönüştürmektir. Çözümleyici, kapsam ve benzersizlik hareketli parçalarıdır; güvenlik şeridi, gözlerinizi kapatmanızı sağlar.
Kaynak: Orijinal Makale


