Мультиарендность в Laravel: сравниваем три подхода
Одна база с колонкой tenant_id, отдельная схема на арендатора или отдельная БД — у каждого варианта своя цена. Разбираем, где что ломается и как выбрать под свой проект на Laravel 11.

Вы запускаете SaaS: десять компаний-клиентов, у каждой свои пользователи, заказы, настройки. Данные одного клиента не должны утекать к другому — ни через баг в запросе, ни через забытый where. Вопрос, который решается на старте и потом стоит дорого: как разделить данные арендаторов. От ответа зависит структура миграций, стоимость бэкапов, скорость миграции схемы и то, насколько легко случайно показать чужие заказы.
Три базовых подхода: одна общая база с признаком арендатора в каждой таблице, отдельная схема PostgreSQL на арендатора и отдельная база данных на арендатора. Ниже — цена каждого в деньгах, в сложности кода и в том, что сломается первым под ростом числа клиентов. Пишу для Laravel 11.x, примеры с расчётом на PostgreSQL 16, но идеи переносимы.
Подход 1. Одна база, колонка tenant_id
Все арендаторы живут в одних таблицах. В каждую строку пишется tenant_id, и любой запрос обязан фильтровать по нему. Разделение — на уровне приложения, а не базы.
Ключевой инструмент здесь — глобальный скоуп Eloquent, который сам добавляет where tenant_id = ? ко всем запросам модели, чтобы это не приходилось помнить руками.
<?php
// app/Models/Scopes/TenantScope.php — Laravel 11.x
namespace App\Models\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class TenantScope implements Scope
{
public function apply(Builder $builder, Model $model): void
{
// tenant_id берём из контейнера — туда его кладёт middleware
if (app()->bound('currentTenantId')) {
$builder->where(
$model->qualifyColumn('tenant_id'),
app('currentTenantId')
);
}
}
}Скоуп подключается к модели через атрибут #[ScopedBy] или в методе booted(). Middleware при аутентификации кладёт идентификатор арендатора в контейнер: app()->instance('currentTenantId', $user->tenant_id).
Слабое место — сырые запросы через DB::table() и связи, которые обходят скоуп. Скоуп работает только на уровне Eloquent-моделей. Любой DB::select(...), любой отчёт на голом SQL фильтруется вами вручную, и один забытый where — это утечка данных между клиентами.
Что это стоит
- Деньги: дёшево. Одна база, один пул соединений, общие бэкапы.
- Миграции: одна миграция на всех. Изменили схему — применили один раз.
- Риск: высокий. Изоляция держится на дисциплине кода. Ошибка в одном запросе видна всем клиентам сразу.
Подход 2. Схема PostgreSQL на арендатора
Каждый арендатор получает свою схему в одной базе: tenant_42.orders, tenant_17.orders. Таблицы физически разделены, а переключение делается сменой search_path перед запросами арендатора.
<?php
// Переключение схемы в middleware — PostgreSQL 16, Laravel 11.x
use Illuminate\Support\Facades\DB;
$schema = 'tenant_' . $user->tenant_id;
// search_path определяет, в какой схеме искать таблицы без префикса
DB::statement('SET search_path TO ' . $schema . ', public');После установки search_path обычные Eloquent-модели работают как раньше — они попадают в нужную схему без изменений в коде запросов. Это сильная сторона: изоляция обеспечивается базой, а не вашей внимательностью.
Подводный камень — соединения из пула. Если используете постоянные соединения или пул на уровне PgBouncer в режиме transaction, search_path может протечь между запросами разных арендаторов. Устанавливайте его в начале каждого запроса и не полагайтесь на то, что прошлое значение сохранилось.
Что это стоит
- Деньги: средне. Одна база, но много схем — растёт число объектов, тяжелеет системный каталог PostgreSQL.
- Миграции: болезненно. Изменение схемы прогоняется по каждой схеме арендатора отдельно. На сотнях арендаторов это долгий процесс, который надо делать батчами.
- Риск: средний. Утечка между арендаторами маловероятна, но забытый
SET search_pathотправит запрос не туда.
Подход 3. Отдельная база на арендатора
Каждый клиент — своя база данных, а иногда и свой сервер. Максимальная изоляция: физически невозможно одним запросом достать данные двух клиентов. Laravel умеет переключать подключение на лету через config(['database.connections...']) и DB::purge().
Именно этот подход из коробки закрывает популярный пакет stancl/tenancy: он подменяет соединение по домену или заголовку и изолирует кеш, очереди и файловое хранилище на арендатора. Ставить свою реализацию с нуля есть смысл, только если требования расходятся с тем, что даёт пакет.
Отдельная база — не «на всякий случай надёжнее». Это осознанный размен: вы платите операционной сложностью за то, чтобы утечка данных между клиентами стала физически невозможной. Для медицины, финансов и госзаказа этот размен часто обязателен по требованиям, а не по вкусу.
Что это стоит
- Деньги: дорого. Сотни баз — это сотни соединений, отдельные бэкапы, мониторинг на каждую. Пул соединений становится узким местом.
- Миграции: дорого и долго. Прогон по каждой базе, обычно очередью в фоне, с отслеживанием, где миграция упала.
- Риск: низкий по утечкам, высокий по эксплуатации. Забыть про одну из двухсот баз при обновлении схемы — реальная беда.
Сравнение
| Критерий | Общая колонка tenant_id | Схема на арендатора | База на арендатора |
|---|---|---|---|
| Изоляция данных | Только в коде | На уровне схемы | Физическая |
| Стоимость инфраструктуры | Низкая | Средняя | Высокая |
| Сложность миграций | Одна на всех | По каждой схеме | По каждой базе |
| Бэкап одного клиента | Выборка по tenant_id | Дамп схемы | Дамп базы |
| Предел по числу арендаторов | Десятки тысяч+ | Сотни-тысячи | Сотни |
| Цена ошибки в запросе | Утечка данных | Запрос не туда | Практически нет |
Когда что выбирать
- Общая колонка: B2C или мелкий B2B, много арендаторов, данные не критичны по регуляторике, команда дисциплинированно фильтрует запросы. Дефолтный выбор для большинства SaaS на старте.
- Схема на арендатора: вы на PostgreSQL, арендаторов десятки-сотни, нужна изоляция крепче кода, но плодить базы дорого. Разумный компромисс.
- База на арендатора: изоляция требуется по договору или закону, клиентов немного, каждый крупный, готовы платить эксплуатацией. Не берите ради «надёжности вообще».
Смена подхода на живом проекте — дорогая миграция данных и переписывание слоя доступа. Выбирайте под ожидаемое число арендаторов и требования к изоляции, а не под сегодняшних трёх клиентов.
Что ломается первым
Независимо от подхода первыми валятся операции, которые пересекают границу арендатора: сквозная аналитика по всем клиентам, глобальный поиск, отчёты для админа платформы. В модели с колонкой tenant_id они пишутся легко, но обходят скоуп — их надо ревьюить особенно строго. В моделях со схемами и базами такие отчёты вообще требуют отдельного пайплайна: вы либо агрегируете данные в отдельное хранилище, либо гоняете запрос по всем схемам и складываете результаты.
Второе больное место — фоновые задачи. Джоб, поставленный в очередь одним арендатором, при обработке должен восстановить его контекст (tenant_id или соединение), иначе выполнится в контексте по умолчанию или в чужом. Кладите идентификатор арендатора в payload джоба и восстанавливайте его в начале handle().
Что делать дальше
Выбрали подход — закройте изоляцию тестами, а не надеждой. Напишите тест, который создаёт двух арендаторов, пишет данные от одного и проверяет, что запрос от второго их не видит. Такой тест дешевле любого инцидента с утечкой. Дальше настройте прогон миграций для выбранной модели: для схем и баз это фоновая команда с отчётом об ошибках, а не ручной php artisan migrate. И измеряйте число открытых соединений к базе под нагрузкой — именно оно первым упрётся в лимит при росте числа арендаторов.
Создатель презентаций: Структура и текст каждого слайда: заголовки-утверждения, одна мысль на слайд, заметки докладчику.
Спросить за 150 ₽Источники: Laravel 11.x — Eloquent: Global Scopes, Tenancy for Laravel — документация пакета stancl/tenancy
Частые вопросы
А если у меня не PostgreSQL, а MySQL — подход со схемами подойдёт?
Глобальный скоуп добавил where, но данные всё равно текут — что не так?
Можно ли начать с общей колонки, а потом переехать на отдельные базы?
Как прогонять миграции, когда арендаторов сотни?
Что будет с фоновыми задачами при мультиарендности?
Насколько pgbouncer в transaction-режиме безопасен со схемами?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.