Xử lý Multi-Tenancy trong Laravel: Chiến lược Database Per Tenant
Phân tích kiến trúc Multi-Tenancy cho ứng dụng SaaS. Tại sao nên chọn Database Per Tenant và cách triển khai dynamic connection trong Laravel.
Làm sản phẩm SaaS (Software as a Service) là ước mơ của nhiều dev. Code một lần, bán cho nhiều người thuê. Nhưng khi bắt tay vào làm, câu hỏi đầu tiên về kiến trúc luôn là: Lưu dữ liệu khách hàng như thế nào?
Có 3 chiến lược chính:
- Single DB, Shared Schema: Thêm cột
tenant_idvào mọi bảng. (Dễ code, khó scale, rủi ro leak data cao). - Single DB, Separate Schema: Chung DB nhưng khác Schema (Postgres support tốt).
- Database Per Tenant: Mỗi khách hàng một Database riêng biệt.
Trong bài này, tôi sẽ bảo vệ quan điểm chọn option 3 (Database Per Tenant) cho các dự án B2B nghiêm túc, và cách Laravel xử lý nó.
Tại sao chọn Database Per Tenant?
Tôi từng làm một dự án SaaS quản lý nhân sự. Ban đầu chọn cách 1 (tenant_id). Mọi thứ êm đẹp cho đến khi:
- Một khách hàng lớn muốn backup dữ liệu của riêng họ để đem về server nội bộ -> Khóc thét vì phải đi lọc từng dòng trong DB chung.
- Dev lỡ tay viết query quên
where('tenant_id', ...)-> Thảm họa: Khách A thấy lương nhân viên khách B.
Với Database Per Tenant, dữ liệu được cô lập vật lý. Khách hàng A dùng DB db_tenant_a, Khách B dùng db_tenant_b. An toàn tuyệt đối.
Triển khai trong Laravel
Ý tưởng cốt lõi: Khi Request đến, ta xác định xem Request này của Tenant nào (qua Subdomain hoặc Header), sau đó switch database connection sang DB của Tenant đó on-the-fly.
1. Cấu hình config/database.php
Tạo một connection “động” tên là tenant.
'connections' => [
'tenant' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'database' => null, // Sẽ điền lúc runtime
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
// ...
],
],
2. Middleware xác định và Switch DB
Tạo middleware IdentifyTenant.
class IdentifyTenant
{
public function handle($request, Closure $next)
{
// Giả sử nhận diện qua subdomain: tenant1.app.com
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
// Tìm thông tin Tenant trong DB master (DB quản lý danh sách khách hàng)
$tenant = Tenant::where('subdomain', $subdomain)->firstOrFail();
// Switch Connection
Config::set('database.connections.tenant.database', $tenant->database_name);
// Purge cache cũ và connect lại
DB::purge('tenant');
DB::reconnect('tenant');
// Set default connection cho Eloquent model dùng
DB::setDefaultConnection('tenant');
return $next($request);
}
}
3. Vấn đề Migration
Như đã nói ở Quiz, chạy migration cho 1000 DB là cơn ác mộng.
Tôi thường viết một Artisan command riêng: php artisan tenants:migrate.
// Command Logic
$tenants = Tenant::all();
foreach ($tenants as $tenant) {
$this->info("Migrating for: " . $tenant->name);
Artisan::call('migrate', [
'--database' => 'tenant',
'--path' => 'database/migrations/tenant', // Folder migration riêng cho tenant
'--force' => true,
]);
// Switch config lại cho vòng lặp sau (quan trọng!)
Config::set('database.connections.tenant.database', $tenant->database_name);
DB::purge('tenant');
}
Lưu ý quan trọng
- Connection Management: Việc đóng mở kết nối liên tục có thể tốn resource. Cần cấu hình DB Pool hoặc dùng các dịch vụ quản lý connection ở Production.
- Job Queues: Khi đẩy Job vào Queue, nhớ kèm theo
tenant_idhoặc context để khi Worker xử lý, nó biết switch sang DB nào. Có góispatie/laravel-multitenancyhỗ trợ việc này rất tốt.
Kết luận
Database Per Tenant phức tạp ở khâu DevOps và Migration, nhưng nó mang lại sự yên tâm tuyệt đối về bảo mật và khả năng Scale dữ liệu (Sharding) sau này. Nếu bạn xác định làm SaaS lâu dài, hãy cân nhắc mô hình này.
Mô hình 'Database Per Tenant' có ưu điểm gì lớn nhất?
Thử Thách Kiến Thức Lịch Sử?
Khám phá hàng trăm câu hỏi trắc nghiệm lịch sử thú vị tại HistoQuiz. Vừa học vừa chơi, nâng cao kiến thức ngay hôm nay!