Hibernate Discrimitator Multitenancy и Hibernate Filters

singletenant-multitenant(мультиарендность) — это подход к проектированию приложения, когда один экземпляр приложений обслуживает несколько клиентов с непересекающимися наборами данных. Например сайт по учёту персональных финансов имеет одну копию кода, одно хранилище данных и много клиентов, при этом каждому клиенту доступны только его собственные данные. Наиболее популярен этот подход, по очевидным причинам, в облачных SaaS решениях — каждый клиент видит общий, единый, экземпляр приложения как свою собственную копию.

Чаще всего проблемы с реализацией кроются не на уровне логики приложения, а на уровне хранения данных: мы должны уметь отделять данные одного клиента от другого и не давать им шанса смешаться. Существует три основных подхода к решению этой проблемы:

  • Разделение на уровне базы данных — под каждого клиента создаётся (или запускается) отдельный экземпляр БД, уникальный для этого клиента и в нём распологаются данные только этого клиента.
  • Разделение на уровне схемы — в одной и той же базе данных создаются разные схемы (schema/namespace) в которых создаются копии структуры таблиц, необходимых для работы клиента и данные каждого клиента живут в отдельно схеме.
  • Разделение на уровне таблиц — и база данных одна, и схема одна, и даже таблицы те же самые. Но в каждой таблице заводится столбец (дискриминатор), указывающий, какому клиенту принадлежит какая таблица.

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

Фильтры в

Hibernate позволяет определить простое SQL условие, которое будет прикладываться к любом запросу. Фактически фильтр это просто параметризованное SQL условие с именем, которое в рамках одной сессии прикладывается к каждому выполняемому запросу.

Определяется фильтр в два шага: вначале задаётся имя фильтра и список его параметров, затем собственно текст запроса для фильтра. Например, зная что мы делаем основанную на дискриминаторе multitenancy, и что в каждой таблице есть столбец дискриминатора, который называется, допустим, tenantId, мы могли бы написать следующий фильтр:

Фильтр называется discriminator_filter, принимает параметр tenantIdValue типа String. Фильтр добавит к запросу SQL условие tenantId = :tenantIdValue.

Чтобы использовать фильтр, необходимо разрешить его применение в сессии:

Discriminator multitenancy

Выше пример использования фильтров в Hibernate показывает, как ограничивать запросы по tenant id, хранящемуся в таблицах. Но этого недостаточно — его надо хранить и корректно заполнять.

В отличие от других подходов к multitenancy, этот подход требует внесения изменений в схему данных, в частности добавления столбца дискриминатора. Это проще всего сделать создав отдельный @MappedSuperclass для хранения этого столбца и унаследовать все остальные сущности от него.

Я использовал схему данных из других примеров multitenancy и добавил в неё дополнительный класс. В коде выше хорошо видно, что классы с @MappedSuperclass могут быть унаследованы друг от друга и создавать достаточно сложные иерархии наследования.

Последней частью имитации discriminator based multitenancy станет интерцептор, который будет заполнять tenant id при сохранении сущности в базу.

Код достаточно сложен на первый взгляд, но на самом деле ничего сверхестественного здесь не происходит. В метод preFlush() передаётся итератор с сохраняемыми сущностями, для каждой из которых проверяется тип и, если она содержит столбец дискриминатора и он пуст, он заполняется текущим значением tenant id. В качестве источника tenant id используется, весьма в стиле Hibernate, реализация интерфейса CurrentTenantIdentifierResolver, которая (в моём случае) просто хранит заранее заданное значение.

Наконец попробуем использовать всё что мы написали выше:

Код создания объектов

[свернуть]
Запуск примера показывает, что данные для одного и того же запроса возвращаются разные, в зависимости от tenant id:
Код примера доступен на github.