Son birkaç hafta boyunca, Laravel için modüler bir SaaS motoru olan LaraFoundry üzerine çalıştım. En büyük zorluk? Çoklu kiracılığı doğru bir şekilde uygulamak.
Bu makalede, yaklaşımımı paylaşacağım: veritabanı yapısı, özelliklerle otomatik izolasyon ve kapsamlı test stratejileri.
Çoklu Kiracılık Zorluğu
Çoklu Kiracılık Zorluğu
SaaS uygulamaları geliştirirken, verileri kiracılar (şirketler, organizasyonlar, hesaplar) arasında izole etmeniz gerekir. Bunu yanlış yaparsanız, Şirket A Şirket B’nin verilerini görebilir. 😱
Üç ana yaklaşım vardır:
- Kiracı başına ayrı veritabanları – Tam izolasyon, karmaşık işlemler
- Kiracı başına ayrı şemalar – Orta düzeyde izolasyon, orta karmaşıklık
- Paylaşılan veritabanı, satır düzeyinde izolasyon – Basit işlemler, disiplin gerektirir
LaraFoundry için 3. seçeneği tercih ettim. İşte nedeni.
Neden Paylaşılan Veritabanı?
Neden Paylaşılan Veritabanı?
Avantajlar:
- ✅ Daha basit işlemler (yönetilecek tek bir veritabanı)
- ✅ 100-1000 kiracı için daha iyi
- ✅ Maliyet etkin
- ✅ Yedekleme ve göçler daha kolay
- ✅ Kiracılar arası raporlama (yöneticiler için)
Dezavantajlar:
- ⚠️ Dikkatli kodlama gerektirir (elle filtreleme yok!)
- ⚠️ Küresel kapsamlar çok önemlidir
- ⚠️ Test etmek şarttır
Kohana (LaraFoundry için test alanım) için, paylaşılan veritabanı mantıklıydı. 10,000+ kiracıya ölçeklenmem gerekirse, durumu tekrar gözden geçiririm.
Veritabanı Şeması
Veritabanı Şeması
İşte temel yapı:
CREATE TABLE companies (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
subdomain VARCHAR(100) UNIQUE,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
CREATE TABLE users (
id BIGINT PRIMARY KEY,
company_id BIGINT NOT NULL,
name VARCHAR(255),
email VARCHAR(255),
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
UNIQUE(company_id, email),
INDEX(company_id)
);
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
company_id BIGINT NOT NULL,
user_id BIGINT,
total DECIMAL(10,2),
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
INDEX(company_id, created_at)
);
Ana kararlar:
- Her kiracı kapsamındaki tabloda company_id – Null olamaz
- Cascade ile yabancı anahtarlar – Şirket silinirse tüm veriler silinir
- Kompozit dizinler – (company_id, created_at) performans açısından önemlidir
- Tekil kısıtlamalar – (company_id, email) şirket içinde tekrarları engeller
BelongsToCompany Özelliği
BelongsToCompany Özelliği
Manuel where('company_id', ...) kullanmak hata yapma olasılığını artırır. Bir hata = veri sızıntısı.
Çözüm: Küresel kapsamlar kullanarak bir trait geliştirmek.
namespace App\Traits;
use Illuminate\Database\Eloquent\Builder;
trait BelongsToCompany
{
protected static function bootBelongsToCompany(): void
{
static<:>
addGlobalScope('company', function (Builder $builder) {
if (auth()<:>check() <:>&&
auth<:>())->user()->company_id) <:>(){
$builder<:><:>->
where($builder<:><:>getModel()->getTable() . '.company_id',
auth()<:><:>->
user()->company_id
}
});
};
$this->company
;
use
BelongsToCompany
;
Faireuse:
class Order
// ← Tek bir satır!
Kaynak: Orijinal Makale


