Представьте, что вы разрабатываете SaaS-приложение.

Одна компания регистрируется.

Затем регистрируется другая компания.

Затем еще одна.

Все они используют одно и то же приложение, но их данные должны оставаться полностью разделенными.

Компания А никогда не должна видеть следующие данные Компании Б:

  • Пользователей
  • Заказы
  • Клиентов
  • Товары
  • Счета
  • Отчеты
  • Настройки

Именно здесь ключевое значение приобретает  мультитенантность (многоарендность).

Мультитенантность — это не просто добавление колонки tenant_id в таблицу.

Это архитектурное решение, которое затрагивает вашу базу данных, аутентификацию, авторизацию, запросы, хранилище, кэш, очереди, API и даже стратегию развертывания.

В этой статье мы рассмотрим, как работает мультитенантность и как правильно спроектировать ее в приложении на Laravel.

1. Что такое мультитенантность?

Тенант (арендатор) — это изолированный клиент или организация, использующие одно и то же приложение.

Например:

SaaS Application
│
├── Company A
│   ├── Users
│   ├── Products
│   ├── Orders
│   └── Reports
│
├── Company B
│   ├── Users
│   ├── Products
│   ├── Orders
│   └── Reports
│
└── Company C
    ├── Users
    ├── Products
    ├── Orders
    └── Reports

Само приложение является общим.

Данные изолированы логически.

Например:

users
--------------------------------
id | tenant_id | name
--------------------------------
1  | 10        | Jalis
2  | 10        | Rahim
3  | 20        | Karim
4  | 20        | Hasan

Пользователи 1 и 2 принадлежат Тенанту 10.

Пользователи 3 и 4 принадлежат Тенанту 20.

Когда Халис входит в систему, у него должен быть доступ только к данным, принадлежащим Тенанту 10.

2. Мультитенантность отличается от поддержки филиалов

Это важное различие.

 Тенант обычно представляет собой независимую организацию или клиента.

 Филиал обычно является подразделением внутри тенанта.

Например:

Tenant: ABC Corporation
│
├── Dhaka Branch
├── Chittagong Branch
└── Sylhet Branch

Таким образом, архитектура может выглядеть следующим образом:

Tenant
   │
   ├── Branch
   │      ├── Users
   │      ├── Orders
   │      └── Inventory
   │
   └── Branch
          ├── Users
          ├── Orders
          └── Inventory

Это различие становится чрезвычайно важным в системах ERP, POS, HRM, складского учета и SaaS.

У пользователя может быть доступ к:

Tenant → All branches

или:

Tenant → Only one branch

Это разные правила авторизации.

3. Зачем нужна мультитенантность?

Без изоляции тенантов у вас может быть приложение следующего вида:

users
products
orders
customers
invoices

Но как узнать, какой компании принадлежит каждая запись?

Вам нужна связь, такая как:

tenant_id

Например:

products
--------------------------------
id | tenant_id | name
--------------------------------
1  | 10        | Laptop
2  | 10        | Mouse
3  | 20        | Keyboard
4  | 20        | Monitor

Теперь приложение может определять принадлежность.

Запрос вроде:

Product::where('tenant_id', $tenantId)->get();

возвращает только те товары, которые принадлежат этому тенанту.

Но ручное добавление этого условия повсюду может стать опасным.

Здесь на помощь приходят глобальные скоупы (global scopes) и архитектура тенантности в Laravel.

4. Варианты архитектуры мультитенантности

Существует три распространенные стратегии работы с базами данных.

1. Shared Database + Shared Tables2. Shared Database + Separate Schemas3. Separate Database Per Tenant

Каждая из них имеет свои преимущества и недостатки.

5. Стратегия 1 — Общая база данных, общие таблицы

Это один из наиболее распространенных подходов для SaaS-приложений.

Все тенанты используют одну и то же базу данных и одни и те же таблицы.

Каждая таблица, принадлежащая тенанту, содержит:

tenant_id

Например:

orders
--------------------------------
id
tenant_id
customer_id
total
status
created_at

Данные могут выглядеть следующим образом:

Tenant 10
    Order 1
    Order 2
    Order 3Tenant 20
    Order 4
    Order 5Tenant 30
    Order 6
    Order 7

Приложение разделяет данные логически.

Преимущества

  • Простая инфраструктура
  • Более низкая стоимость хостинга
  • Простое управление базой данных
  • Простые миграции
  • Удобное создание отчетов по всем тенантам при необходимости
  • Подходит для множества малых и средних тенантов

Недостатки

Самый большой риск — это утечка данных.

Если в каком-то запросе случайно пропустить:

where('tenant_id', ...)

один тенат потенциально сможет получить доступ к данным другого тенанта.

Именно поэтому изоляция тенантов должна быть тщательно продумана.

6. Стратегия 2 — Отдельная схема для каждого тенанта

В этой архитектуре тенанты могут иметь отдельные схемы внутри одной СУБД.

Концептуально:

Database
│
├── tenant_10
│   ├── users
│   ├── orders
│   └── products
│
├── tenant_20
│   ├── users
│   ├── orders
│   └── products
│
└── tenant_30
    ├── users
    ├── orders
    └── products

Приложение переключает активную схему в зависимости от тенанта.

Это может обеспечить более надежную изоляцию, чем простое использование tenant_id.

Однако управление миграциями и большим количеством схем может стать более сложным.

7. Стратегия 3 — Отдельная база данных для каждого тенанта

Здесь каждый тенант получает собственную базу данных.

Application
│
├── Tenant A → Database A
├── Tenant B → Database B
└── Tenant C → Database C

Например:

tenant_10_db
tenant_20_db
tenant_30_db

Преимущества

Надежная изоляция данных.

База данных одного тенанта физически отделена от базы данных другого тенанта.

Это может быть полезно, когда:

  • Тенанты имеют крупный размер
  • Требования к изоляции данных высоки
  • Клиентам требуются выделенные базы данных
  • Резервное копирование и восстановление должны выполняться для каждого тенанта отдельно
  • Требования комплаенса (соответствия стандартам) строги

Недостатки

Инфраструктура становится более сложной.

Теперь вам нужно управлять:

Multiple Databases
Multiple Connections
Migrations
Backups
Monitoring
Provisioning
Credentials

Если у вас тысячи тенантов, управление базами данных становится серьезной архитектурной проблемой.

8. какую стратегию выбрать?

Универсального ответа нет.

Простым решением может быть:

Small / Medium SaaS
        ↓
Shared Database + tenant_idStrong Isolation Requirements
        ↓
Separate SchemaLarge Enterprise / Dedicated Infrastructure
        ↓
Separate Database

Но при принятии решения также следует учитывать:

  • Количество тенантов
  • Объем данных
  • Соответствие требованиям (комплаенс)
  • Стоимость
  • Стратегию резервного копирования
  • Производительность
  • Операционную сложность
  • Требования клиентов

Не выбирайте отдельные базы данных только потому, что это звучит более масштабируемо.

Иногда грамотно спроектированная архитектура с общей базой данных является лучшим решением.

9. Проектирование таблицы тенантов

Базовая таблица тенантов может выглядеть так:

tenants
-------------------------
id
name
slug
domain
status
created_at
updated_at

Например:

id: 10
name: ABC Corporation
slug: abc-corporation
domain: abc.example.com
status: active

Слаг (slug) или домен помогают идентифицировать тенанта.

10. Как определить текущего тенанта?

Приложение должно отвечать на вопрос:

«Какой тенант в данный момент выполняет этот запрос?»

Существует несколько способов.

Поддомен

abc.example.com
xyz.example.com

Поддомен идентифицирует тенанта.

Пользовательский домен

app.abc.com
app.xyz.com

Аутентифицированный пользователь

Вошедший в систему пользователь принадлежит тенанту:

users
-------------------------
id
tenant_id
name
email

Тогда:

$user->tenant_id

может идентифицировать текущего тенанта.

Сессия

Приложение также может сохранять выбранного тенанта в сессии.

Правильный метод зависит от архитектуры продукта.

11. Контекст тенанта

Один из полезных паттернов — устанавливать текущего тенанта на раннем этапе жизненного цикла запроса.

Например:

HTTP Request
     ↓
Identify Tenant
     ↓
Set Tenant Context
     ↓
Authentication
     ↓
Authorization
     ↓
Controller
     ↓
Business Logic

После этого остальная часть приложения может получить доступ к контексту текущего тенанта.

Концептуально:

$tenant = app(CurrentTenant::class)->get();

Точная реализация может отличаться.

Важная идея заключается в следующем:

 Идентификация тенанта должна происходить централизованно, а не независимо внутри каждого контроллера.

12. Глобальные скоупы могут помочь

Предположим, у нас есть:

products
-------------------------
id
tenant_id
name
price

Вместо того чтобы писать:

Product::where('tenant_id', $tenantId)->get();

всюду, мы можем использовать глобальный скоуп.

Концептуально:

protected static function booted(): void
{
    static::addGlobalScope('tenant', function ($query) {
        $query->where(
            'tenant_id',
            app(CurrentTenant::class)->id()
        );
    });
}

Теперь:

Product::all();

автоматически учитывает тенанта.

Это может значительно снизить риск случайной утечки данных.

Тем не менее, глобальные скоупы нужно использовать с осторожностью.

13. Не доверяйте фронтенду

Это важнейший принцип безопасности.

Никогда не предполагайте, что раз фронтенд отправляет:

{
    "tenant_id": 10
}

пользователю разрешено использовать Тенант 10.

Злоумышленник может изменить запрос:

{
    "tenant_id": 20
}

Принадлежность тенанту должна определяться на основе доверенного контекста на стороне сервера.

Например:

Authenticated User
        ↓
User's Tenant
        ↓
Current Tenant
        ↓
Query Data

А не:

Frontend tenant_id
        ↓
Trust it

14. Модели с учетом тенантов

В архитектуре с общей базой данных вы можете создать общий паттерн для моделей, принадлежащих тенантам.

Например:

TenantModel
    ↓
Product
Order
Customer
Invoice
Payment

Базовая модель может обрабатывать стандартное поведение тенанта.

Концептуально:

abstract class TenantModel extends Model
{
    protected static function booted(): void
    {
        static::creating(function ($model) {
            $model->tenant_id = app(CurrentTenant::class)->id();
        });
    }
}

Затем:

class Product extends TenantModel
{
}

При создании продукта тенант может назначаться автоматически.

Опять же, точная архитектура зависит от приложения.

15. Автоматическое назначение tenant_id

Одна из распространенных ошибок — вручную делать это повсюду:

Product::create([
    'name' => $request->name,
    'price' => $request->price,
    'tenant_id' => auth()->user()->tenant_id,
]);

Затем другой разработчик забывает об этом где-то:

Product::create([
    'name' => $request->name,
    'price' => $request->price,
]);

Теперь у вас может появиться запись без принадлежности к тенанту.

Централизованная стратегия назначения тенантов может снизить этот риск.

16. Связи также должны быть безопасными с точки зрения тенантов

Представьте:

Tenant A
    Customer #1Tenant B
    Customer #2

Теперь заказ (Order) принадлежит клиенту (Customer).

Опасная реализация может позволить:

Tenant A
    ↓
Order
    ↓
Customer #2

если приложение проверяет только ID клиента.

Приложение должно проверять принадлежность.

Концептуально:

Order Tenant
      =
Customer Tenant

Связи в мультитенантных приложениях должны уважать границы тенантов.

17. Уникальные ограничения должны учитывать тенанты

Предположим, каждый тенант может иметь уникальный SKU товара.

Вы можете подумать:

UNIQUE(sku)

Но это означает, что уникальные SKU должны быть во всей базе данных.

Возможно, Тенанту А и Тенанту Б должно быть разрешено использовать:

SKU-100

В этом случае ограничение должно быть следующим:

UNIQUE(tenant_id, sku)

Теперь:

Tenant A → SKU-100
Tenant B → SKU-100

разрешено.

Но:

Tenant A → SKU-100
Tenant A → SKU-100

отклоняется.

Это небольшое решение на уровне базы данных с большим архитектурным влиянием.

18. Мягкое удаление (Soft Deletes) и тенантность

Предположим, продукт подвергается мягкому удалению.

Вам нужно учитывать оба фактора:

tenant_id
deleted_at

Запросы должны учитывать:

Current Tenant
+
Not Deleted

Функционал SoftDeletes в Laravel обрабатывает вторую часть.

Ваша архитектура тенантности должна обрабатывать первую.

19. Файловое хранилище также должно учитывать тенантов

Изоляция тенантов заключается не только в записях базы данных.

Представьте, что пользователи загружают:

profile.jpg
invoice.pdf
product.jpg

Не помещайте файлы каждого тенанта в одну неструктурированную директорию.

Лучшая структура может выглядеть так:

storage/
    tenants/
        10/
            products/
            invoices/
            users/        20/
            products/
            invoices/
            users/

Теперь хранилище логически разделено.

Это особенно важно при генерации публичных URL или подписанных URL для скачивания.

20. Кэш также может утекать данные тенантов

Это часто забываемая проблема.

Представьте:

Cache::remember('products', 3600, function () {
    return Product::all();
});

Это выглядит нормально.

Но в мультитенантном приложении Тенант А может заполнить кэш.

Затем Тенант Б может получить те же закэшированные данные.

Вместо этого ключи кэша должны включать контекст тенанта:

products:tenant:10
products:tenant:20

Тот же принцип применим к:

  • Redis
  • Кэшу приложения
  • Кэшу запросов
  • Статистике дашборда
  • Кэшу разрешений

 Каждое закэшированное значение, специфичное для тенанта, требует ключей с учетом тенанта.

21. Очереди также нуждаются в контексте тенанта

Представьте, что пользователь загружает большой CSV-файл.

Laravel создает задачу в очереди:

ImportProductsJob

Запрос уже знает тенанта.

Но воркер очереди может обработать задачу позже.

Поэтому задача должна знать:

Which tenant created this job?

Распространенный подход — сохранять:

tenant_id

вместе с соответствующими данными задачи.

Концептуально:

User Request
    ↓
Tenant 10
    ↓
Create Job
    ↓
Queue
    ↓
Worker
    ↓
Restore Tenant Context
    ↓
Process Data

Это становится чрезвычайно важным при фоновой обработке.

22. Отчеты должны учитывать тенантов

Представьте дашборд, показывающий:

Total Sales
Total Customers
Total Orders
Revenue

Запрос вроде:

Order::sum('total');

опасен в общей базе данных.

Он может рассчитать общую сумму для каждого тенанта.

Запрос должен учитывать текущего тенанта.

Таким образом, изоляция тенантов должна существовать не только в операциях CRUD, но также в:

  • Отчетах
  • Аналитике
  • Дашбордах
  • Экспортах
  • Поиске
  • Уведомлениях

23. API нужна изоляция тенантов

Предположим, ваше API имеет:

GET /api/orders
GET /api/customers
GET /api/products

Самой по себе аутентификации недостаточно.

API также должен определять:

Who is the user?
Which tenant does the user belong to?
What can the user access inside that tenant?

Например:

API Request
     ↓
Authentication
     ↓
Tenant Identification
     ↓
Permission Check
     ↓
Tenant-Scoped Query
     ↓
Response

Этот паттерн должен быть единообразным во всем API.

24. Суперадмин — особый случай

В SaaS-приложении часто есть администратор уровня платформы.

Например:

Platform
│
├── Tenant A
├── Tenant B
└── Tenant C

Администратору платформы может потребоваться видеть всех тенантов.

Это создает важное различие:

Tenant User
    ↓
Only current tenantPlatform Admin
    ↓
Can manage tenants

Будьте осторожны при обходе скоупов тенантов.

Такой метод, как:

withoutGlobalScopes()

может быть мощным инструментом, но его не следует использовать бездумно.

Всякий раз, когда изоляция тенантов обходится, должны быть веские основания и строгая авторизация.

25. Тестирование мультитенантности

Мультитенантность требует большего, чем обычное функциональное тестирование.

Например:

Create Tenant A
Create Tenant BCreate Product for Tenant A
Create Product for Tenant B

Затем протестируйте:

Tenant A user → Can see A product
Tenant A user → Cannot see B productTenant B user → Can see B product
Tenant B user → Cannot see A product

Также протестируйте:

Create
Read
Update
Delete
Search
Export
Reports
API
Files
Queues
Cache

Очень важный тест:

Может ли Тенант А когда-либо получить доступ к данным Тенанта Б?

Этот вопрос следует проверять снова и снова.

26. Распространенные ошибки мультитенантности

Ошибка 1 — Ручное добавление tenant_id везде

Это увеличивает вероятность забыть его.

Ошибка 2 — Доверие tenant_id из запроса

Клиент может его изменить.

Ошибка 3 — Защита только фронтенда

Пользователи могут вызывать API напрямую.

Ошибка 4 — Забытые связи

Заказ может случайно ссылаться на клиента другого тенанта.

Ошибка 5 — Забытый кэш

Закэшированные данные Тенанта А могут появиться у Тенанта Б.

Ошибка 6 — Забытые очереди

Фоновые задачи могут потерять контекст тенанта.

Ошибка 7 — Забытое файловое хранилище

Файлы тенантов могут перемешаться.

Ошибка 8 — Забытые уникальные ограничения

Глобальная уникальность может быть некорректной для данных, специфичных для тенанта.

Ошибка 9 — Отсутствие тестов автоматической изоляции

Будущий разработчик может случайно привнести утечку данных.

27. Практичная архитектура на Laravel

SaaS-приложение с общей базой данных может выглядеть так:

Laravel Application
│
├── Authentication
│
├── Tenant Context
│      └── CurrentTenant
│
├── Authorization
│      └── Roles & Permissions
│
├── Tenant-Aware Models
│      ├── Product
│      ├── Order
│      ├── Customer
│      └── Invoice
│
├── Tenant-Aware Services
│
├── Tenant-Aware Jobs
│
├── Tenant-Aware Cache
│
├── Tenant-Aware Storage
│
└── Tenant-Aware Reports

Ключевой принцип заключается в следующем:

 Учет тенанта должен быть частью архитектуры, а не доработкой задним числом.

28. Простой жизненный цикл запроса

Мультитенантный запрос в Laravel концептуально может работать следующим образом:

HTTP Request
      ↓
Identify Tenant
      ↓
Set CurrentTenant
      ↓
Authenticate User
      ↓
Check Tenant Membership
      ↓
Check Permission
      ↓
Tenant-Scoped Query
      ↓
Business Logic
      ↓
Response

Этот централизованный поток делает приложение более понятным.

29. Мультитенантность — это архитектурное решение

Главный урок заключается в том, что мультитенантность затрагивает гораздо больше, чем просто базу данных.

Вам нужно подумать о:

Database
Authentication
Authorization
Models
Relationships
Unique Constraints
Storage
Cache
Queues
APIs
Reports
Notifications
Testing
Deployment
Backups

Если вы только добавили:

tenant_id

в свои таблицы, вы еще не до конца спроектировали мультитенантную систему.

Вы ее только начали.

Заключение

Мультитенантность — это одно из тех архитектурных решений, которые гораздо проще правильно спроектировать в самом начале, чем переделывать позже.

Прежде чем создавать SaaS-приложение, спросите себя:

Как мы будем идентифицировать текущего тенанта?Где будет храниться принадлежность к тенанту?Как запросы будут автоматически ограничиваться скоупом?Как мы предотвратим межтенантные связи?Как будут разделяться файлы?Как будут разделяться ключи кэша?Как задачи в очереди узнают своего тенанта?Как отчеты и API будут соблюдать границы тенантов?Как мы будем тестировать изоляцию тенантов?

И самое главное:

 Что произойдет, если Тенант А попытается получить доступ к данным Тенанта Б?

Если архитектура может последовательно ответить на этот вопрос, вы на верном пути.

Хорошая мультитенантная система — это не просто система, где несколько клиентов могут использовать одно приложение.

Это система, в которой  несколько клиентов могут безопасно использовать одно приложение, в то время как их данные, права доступа, конфигурация и бизнес-операции остаются должным образом изолированными.

 Сначала спроектируйте изоляцию тенантов. Масштабируйте приложение во вторую очередь.