Мультитенантность с префиксами запросов
Исходный кодС помощью Ecto можно выполнять запросы с разными префиксами, используя единый пул подключений к базе данных. Для таких систем управления базами данных, как Postgres, префикс Ecto соответствует схемам DDL в Postgres. В MySQL каждый префикс представляет собой отдельную базу данных.
Мультитенантность и миграции
При работе с мультитенантными базами данных миграции базы данных (например, добавление или удаление таблиц, столбцов и индексов) необходимо применять для каждого арендатора. Если ваше приложение разрастётся до десятков тысяч арендаторов или более, такие миграции со временем могут стать слишком затратными и выполняться очень долго.
Префиксы запросов могут быть полезны в разных сценариях. Например, приложения с несколькими арендаторами на PostgreSQL могут определять несколько префиксов — обычно по одному на клиента — в рамках одной базы данных. Предполагается, что префиксы обеспечивают изоляцию данных между разными пользователями приложения, гарантируя на глобальном уровне или на уровне данных, что запросы и команды работают с данными конкретного арендатора.
Префиксы также могут быть полезны в приложениях с высокой нагрузкой, где данные заранее разбиты на разделы. Например, игровая платформа может разбить игровые данные на изолированные разделы, назвав каждый из них отдельным префиксом. Раздел для конкретного игрока выбирается случайным образом или вычисляется на основе информации об игроке.
Поскольку у каждого арендатора своя структура базы данных, мультитенантность с префиксами запросов требует значительных затрат на настройку. Например, миграции необходимо запускать отдельно для каждого префикса. Поэтому этот подход полезен, когда число арендаторов ограничено или растёт медленно.
Начнём. Обратите внимание: во всех примерах ниже предполагается, что вы используете PostgreSQL. Для других систем управления базами данных могут потребоваться немного иные решения.
Префиксы подключений
Для начала рассмотрим простой сценарий: в production ваше приложение должно подключаться к определённому префиксу. Это может быть обусловлено особенностями инфраструктуры, правилами администрирования базы данных или другими причинами.
Для начала определим репозиторий и схему:
# lib/repo.ex
defmodule MyApp.Repo do
use Ecto.Repo,
otp_app: :my_app,
adapter: Ecto.Adapters.Postgres
end
# lib/sample.ex
defmodule MyApp.Sample do
use Ecto.Schema
schema "samples" do
field :name
timestamps
end
end
Теперь настроим репозиторий:
# config/config.exs config :my_app, MyApp.Repo, username: "postgres", password: "postgres", database: "demo", hostname: "localhost", pool_size: 10
И определим миграцию:
# priv/repo/migrations/20160101000000_create_sample.exs
defmodule MyApp.Repo.Migrations.CreateSample do
use Ecto.Migration
def change do
create table(:samples) do
add :name, :string
timestamps()
end
end
end
Теперь создадим базу данных, выполним миграцию и запустим сеанс IEx:
$ mix ecto.create $ mix ecto.migrate $ iex -S mix Interactive Elixir - press Ctrl+C to exit iex(1)> MyApp.Repo.all MyApp.Sample []
Пока что мы не сделали ничего необычного. Мы создали экземпляр базы данных, привели его в актуальное состояние с помощью миграций и успешно выполнили запрос к таблице "samples", который вернул пустой список.
По умолчанию подключения к базам данных Postgres работают с префиксом "public". При выполнении миграций и запросов все они работают с префиксом "public". Однако представьте, что в production ваше приложение должно работать с определённым префиксом. Назовём его "connection_prefix".
К счастью, Postgres позволяет изменить префикс, с которым работают подключения к базе данных, задав «путь поиска схем». Лучше всего изменить путь поиска при настройке подключения к базе данных, чтобы все запросы на протяжении всего жизненного цикла подключения выполнялись с нужным префиксом.
Для этого изменим конфигурацию базы данных в "config/config.exs" и зададим параметр :search_path в параметре :parameters. Параметр :search_path принимает строку с разделёнными запятыми префиксами, которые задают путь поиска для подключения:
config :my_app, MyApp.Repo, username: "postgres", password: "postgres", database: "demo_dev", hostname: "localhost", pool_size: 10, parameters: [search_path: "connection_prefix,public"]
Теперь попробуем выполнить тот же запрос, что и раньше:
$ iex -S mix Interactive Elixir - press Ctrl+C to exit iex(1)> MyApp.Repo.all MyApp.Sample ** (Postgrex.Error) ERROR (undefined_table): relation "samples" does not exist
Ранее успешно выполнявшийся запрос теперь завершается ошибкой, поскольку в новом префиксе нет таблицы "samples". Попробуем исправить это, запустив миграции:
$ mix ecto.migrate ** (Postgrex.Error) ERROR (invalid_schema_name): no schema has been selected to create in
Упс. Теперь миграция сообщает, что схема с таким именем не существует. Это происходит потому, что Postgres автоматически создаёт префикс "public" при каждом создании новой базы данных. Если мы хотим использовать другой префикс, его необходимо явно создать в используемой базе данных:
$ psql -d demo_dev -c "CREATE SCHEMA connection_prefix"
Теперь можно выполнить миграцию и запустить запросы:
$ mix ecto.migrate $ iex -S mix Interactive Elixir - press Ctrl+C to exit iex(1)> MyApp.Repo.all MyApp.Sample []
Данные в разных префиксах изолированы. Доступ к данным, записанным в таблицу "samples" одного префикса, невозможен из другого, если не изменить префикс подключения или не воспользоваться возможностями Ecto, которые мы рассмотрим далее.
Префиксы схем
Ecto также позволяет задать для определённой схемы конкретный префикс. Представьте, что вы разрабатываете мультитенантное приложение. Данные каждого клиента относятся к определённому префиксу, например "client_foo", "client_bar" и так далее. При этом приложение может использовать набор таблиц, общих для всех клиентов. Одна из таких таблиц может связывать идентификатор клиента с префиксом его базы данных. Предположим, мы хотим хранить эти данные в префиксе с именем "main":
defmodule MyApp.Mapping do
use Ecto.Schema
@schema_prefix "main"
schema "mappings" do
field :client_id, :integer
field :db_prefix
timestamps
end
end
Вызов MyApp.Repo.all MyApp.Mapping по умолчанию будет работать с префиксом "main" независимо от значения, настроенного для подключения в обратном вызове :after_connect. Однако мы можем захотеть переопределить и префикс схемы. Ecto позволяет это сделать — посмотрим как.
Префиксы для запросов и структур
Теперь предположим, что, будучи настроенными на подключение к "connection_prefix" через :after_connect, мы выполняем следующие запросы:
iex(1)> alias MyApp.Sample
MyApp.Sample
iex(2)> MyApp.Repo.all(Sample)
[]
iex(3)> MyApp.Repo.insert(%Sample{name: "mary"})
{:ok, %MyApp.Sample{...}}
iex(4)> MyApp.Repo.all(Sample)
[%MyApp.Sample{...}]
Операции выше выполнялись с префиксом "connection_prefix". Что произойдёт, если попробовать выполнить запрос к таблице samples с префиксом "public"? Во всех операциях репозитория Ecto поддерживается параметр :prefix. Зададим ему значение public.
iex(7)> MyApp.Repo.all(Sample)
[%MyApp.Sample{...}]
iex(8)> MyApp.Repo.all(Sample, prefix: "public")
[]
Обратите внимание, что нам удалось изменить префикс, с которым выполняется запрос. В префиксе "public", используемом по умолчанию, данных нет.
Одна из интересных особенностей префиксов в Ecto заключается в том, что информация о префиксе сохраняется в каждой структуре, возвращённой запросом:
iex(9)> [sample] = MyApp.Repo.all(Sample)
[%MyApp.Sample{}]
iex(10)> Ecto.get_meta(sample, :prefix)
nil
Пример выше вернул nil, а это означает, что Ecto не указал префикс и поэтому будет использоваться префикс подключения по умолчанию. В данном случае будет использоваться "connection_prefix" благодаря обратному вызову :after_connect, который мы добавили в начале этого руководства.
Поскольку информация о префиксе сохраняется в структуре, мы можем использовать её, чтобы копировать данные из одного префикса в другой. Скопируем приведённую выше запись из "connection_prefix" в "public":
iex(11)> new_sample = Ecto.put_meta(sample, prefix: "public")
%MyApp.Sample{}
iex(12)> MyApp.Repo.insert(new_sample)
{:ok, %MyApp.Sample{}}
iex(13)> [sample] = MyApp.Repo.all(Sample, prefix: "public")
[%MyApp.Sample{}]
iex(14)> Ecto.get_meta(sample, :prefix)
"public"
Теперь данные вставлены в оба префикса. Обратите внимание, что мы передали параметр :prefix в MyApp.Repo.all. Почти все операции Repo принимают :prefix в качестве параметра, но есть одно важное отличие:
параметр
:prefixв операциях с запросами (all/2,update_all/2иdelete_all/2) является резервным. Он используется только в том случае, если ранее не был задан@schema_prefixили префикс запросапараметр
:prefixв операциях со схемами (insert_all/3,insert/2,update/2и т. д.) переопределяет@schema_prefix, а также любой префикс в структуре или changeset
Это различие предусмотрено намеренно: при написании запросов мы хотим обеспечить гибкость, но операции со структурами и changeset должны всегда выполняться изолированно в рамках заданного префикса. В частности, если вызвать MyApp.Repo.insert(post) или MyApp.Repo.update(post) и в записи есть ассоциации, связанные данные также будут вставлены или обновлены в том же префиксе, что и post.
Префиксы для отдельных from/join
Наконец, Ecto позволяет задать префикс отдельно для каждого выражения from и join. Вот пример:
from p in Post, prefix: "foo", join: c in Comment, prefix: "bar"
Эти префиксы имеют приоритет над всеми остальными префиксами, которые мы задали ранее. Для каждого join/from в запросе используемый префикс определяется в следующем порядке:
- Если параметр префикса задан непосредственно для join/from
- Если в связанной схеме задан
@schema_prefix - Если для операции репозитория задано поле
:prefix(например,Repo.all(query, prefix: prefix)) - Префикс подключения
Префиксы миграций
Если задан префикс подключения, меняется и префикс, с которым выполняются миграции. Однако префикс можно также задать в командной строке или для каждой таблицы непосредственно в миграции.
Например, представьте, что вы работаете в игровой компании, где игра разбита на 128 разделов с именами "prefix_1", "prefix_2", "prefix_3" вплоть до "prefix_128". Каждый раз, когда нужно выполнить миграцию данных, её необходимо запустить для всех 128 префиксов. Есть два способа это сделать.
Первый способ — вызвать mix ecto.migrate несколько раз, по одному разу для каждого префикса, передавая параметр --prefix:
$ mix ecto.migrate --prefix "prefix_1" $ mix ecto.migrate --prefix "prefix_2" $ mix ecto.migrate --prefix "prefix_3" ... $ mix ecto.migrate --prefix "prefix_128"
Другой способ — изменить каждую нужную миграцию так, чтобы она выполнялась для нескольких префиксов. Например:
defmodule MyApp.Repo.Migrations.CreateSample do
use Ecto.Migration
def change do
for i <- 1..128 do
prefix = "prefix_#{i}"
create table(:samples, prefix: prefix) do
add :name, :string
timestamps()
end
# Execute the commands on the current prefix
# before moving on to the next prefix
flush()
end
end
end
Итоги
Ecto предоставляет множество удобных средств для работы с префиксами запросов. Эти средства позволяют разработчикам настраивать префиксы с разным приоритетом, начиная с наивысшего. При выполнении запросов с помощью all, update_all или delete_all префикс определяется в следующем порядке:
- префиксы from/join
- префиксы схем
- параметр
:prefix - префиксы подключений
При работе со схемами и changeset в insert_all, insert, update и т. д. приоритеты распределяются так:
- параметр
:prefix - префиксы changeset
- префиксы схем
- префиксы подключений
Таким образом разработчики могут решать самые разные задачи — от требований production-среды до создания мультитенантных приложений.
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/multi-tenancy-with-query-prefixes.html