Я реализую мультиарендный SaaS с полиморфными отношениями между 3 подключениями к базе данных (арендатор1, арендатор2, общий). Проблема возникает только в заданиях очереди, где ограничения поворота не работают из-за потери контекста клиента.
Схема базы данных
SQL
Код: Выделить всё
-- shared database
tenants
id (uuid), name, database_name
-- tenant1 database
users
id (uuid), tenant_id (uuid), name, email
workspaces
id (uuid), tenant_id (uuid), name
-- tenantX databases (dynamic)
workspace_members (pivot)
id (uuid), workspace_id (uuid), member_id (uuid), member_type
-- member_type: 'App\Models\User', 'App\Models\Team', 'App\Models\ExternalGuest'
PHP
Код: Выделить всё
// app/Models/Tenant.php (shared DB)
class Tenant extends Model
{
protected $connection = 'shared';
public function database()
{
return $this->belongsToMany(Workspace::class, 'workspace_members',
'tenant_id', 'workspace_id')->withPivot('member_id', 'member_type');
}
}
// app/Models/Workspace.php (tenant DB)
class Workspace extends Model
{
protected $connection = null; // Dynamic
public function members()
{
return $this->morphToMany(
get_class(), // Dynamic: User|Team|ExternalGuest
'member',
'workspace_members',
'workspace_id',
'member_id'
)->withPivot('tenant_id'); // Tenant isolation
}
public function membersInCurrentTenant()
{
return $this->members()->wherePivot('tenant_id', tenant()->id);
}
}
// Dynamic model resolution
class DynamicModelResolver
{
public static function resolve(string $type): Model
{
$tenantId = tenant()->id;
// Switch DB connection
config(['database.connections.tenant.database' => "tenant_{$tenantId}"]);
DB::purge('tenant');
DB::reconnect('tenant');
return match($type) {
'App\Models\User' => User::on('tenant'),
'App\Models\Team' => Team::on('tenant'),
'App\Models\ExternalGuest' => ExternalGuest::on('tenant')
};
}
}
Работает в HTTP-запросах
PHP
Код: Выделить всё
// In controller - WORKS
$workspace = Workspace::find('uuid');
$members = $workspace->membersInCurrentTenant()->get();
// Returns only members where pivot.tenant_id = current_tenant
PHP
Код: Выделить всё
// In queue job - BROKEN
class ProcessWorkspaceActivity extends Job
{
public function handle()
{
$workspace = Workspace::find('uuid');
$members = $workspace->membersInCurrentTenant()->get();
// Returns ALL members across ALL tenants!
// pivot.tenant_id constraint is ignored
}
}
HTTP-запрос:
текст
Код: Выделить всё
SQL: select * from workspace_members
where workspace_id = ?
and tenant_id = 'tenant-uuid-here'
текст
Код: Выделить всё
SQL: select * from workspace_members
where workspace_id = ?
-- NO tenant_id constraint!
- Промежуточное программное обеспечение арендатора в заданиях
Код: Выделить всё
class TenantAwareJob extends Job
{
public $tenantId;
public function handle()
{
tenant($this->tenantId); // Sets global tenant
// Still broken
}
}
- Глобальные области
Код: Выделить всё
// WorkspaceMemberObserver
class WorkspaceMemberObserver
{
public function retrieved(WorkspaceMember $model)
{
if (!tenant()) return;
$model->where('tenant_id', tenant()->id);
}
}
- Переключение соединения
Код: Выделить всё
DB::connection('tenant')->table('workspace_members')
->where('workspace_id', $id)
->where('tenant_id', tenant()->id); // Works but bypasses Eloquent
PHP
Код: Выделить всё
// DatabaseSeeder
DB::connection('tenant1')->table('workspace_members')->insert([
['workspace_id' => 'ws1', 'member_id' => 'user1', 'member_type' => 'App\Models\User', 'tenant_id' => 'tenant1'],
['workspace_id' => 'ws1', 'member_id' => 'user2', 'member_type' => 'App\Models\User', 'tenant_id' => 'tenant2'], // Cross-tenant!
]);
// Test job
php artisan queue:work --once
Вопросы
- Почему ограничение сводки исчезает в заданиях очереди?
- Как сохранить контекст клиента в отношениях Eloquent во время обработки очереди?
- Лучшая практика для многопользовательских полиморфных отношений с динамическими подключениями к БД?
PHP
Код: Выделить всё
// Forces tenant constraint manually
$members = $workspace->members()
->whereHas('pivot', fn($q) => $q->where('tenant_id', tenant()->id))
->get();
- с нетерпеливой загрузкой ('members')
- members()->count()
- Любая цепочка отношений
Подробнее здесь: https://stackoverflow.com/questions/798 ... t-constrai
Мобильная версия