Сопоставление и проверка данных
Исходный кодРассмотрим роль схем при проверке и приведении типов данных с помощью наборов изменений. Как мы увидим, иногда лучшее решение — не полностью отказываться от схем, а разбить большую схему на несколько меньших. Например, одну — для чтения данных, другую — для записи. Или одну — для базы данных, другую — для форм.
Схемы выполняют сопоставление
В документации модуля Ecto.Schema сказано:
Схема Ecto используется для сопоставления любого источника данных со структурой Elixir.
Мы выделили слово любого, потому что часто ошибочно полагают, что схемы Ecto сопоставляют данные только с таблицами базы данных.
Например, если вы разрабатываете веб-приложение с помощью Phoenix и используете Ecto для получения внешних изменений и их применения к базе данных, такое сопоставление выглядит следующим образом:
Database <-> Ecto schema <-> Forms / API
Хотя одна схема Ecto сопоставляет данные и с базой данных, и с API, во многих случаях лучше разделить это сопоставление на две части. Рассмотрим несколько практических примеров.
Представьте, что вы работаете с клиентом, который хочет, чтобы форма «Регистрация» содержала поля «Имя» и «Фамилия» наряду с полем «Электронная почта» и другой информацией. Вы знаете, что у такого подхода есть несколько проблем.
Прежде всего, не у всех есть имя и фамилия. Хотя клиент решил показывать оба поля, они относятся к интерфейсу, и вы не хотите, чтобы интерфейс определял структуру ваших данных. Кроме того, вы знаете, что было бы полезно распределить данные «Регистрации» по двум таблицам: «accounts» и «profiles».
Как реализовать регистрацию на стороне сервера с учётом этих требований?
Один из вариантов — создать две схемы, Account и Profile, с виртуальными полями, например first_name и last_name, а затем связать схемы с интерфейсом с помощью ассоциаций и вложенных форм. Одна из таких схем может выглядеть так:
defmodule Profile do
use Ecto.Schema
schema "profiles" do
field :name
field :first_name, :string, virtual: true
field :last_name, :string, virtual: true
...
end
end
Нетрудно заметить, что, добавляя поля вроде first_name и last_name, мы загрязняем схему Profile требованиями интерфейса. Если схема Profile используется и для чтения, и для записи данных, она может оказаться в неудобном положении и не подходить ни для одной из этих задач, поскольку содержит поля, соответствующие только одной из них.
Один из альтернативных вариантов — разделить сопоставление «База данных <-> схема Ecto <-> формы / API» на две части. Первая будет приводить типы и проверять внешние данные, используя собственную структуру, после чего вы преобразуете их и запишете в базу данных. Для этого определим схему с именем Registration, которая будет отвечать исключительно за приведение типов и проверку данных формы, напрямую сопоставляя их с полями интерфейса:
defmodule Registration do
use Ecto.Schema
embedded_schema do
field :first_name
field :last_name
field :email
end
end
Мы использовали embedded_schema, поскольку не собираемся сохранять эти данные. Имея схему, мы можем использовать наборы изменений и проверки Ecto для обработки данных:
fields = [:first_name, :last_name, :email]
changeset =
%Registration{}
|> Ecto.Changeset.cast(params["sign_up"], fields)
|> validate_required(...)
|> validate_length(...)
Теперь, когда изменения регистрации сопоставлены и проверены, можно проверить корректность полученного набора изменений и действовать соответствующим образом:
if changeset.valid? do
# Get the modified registration struct from changeset
registration = Ecto.Changeset.apply_changes(changeset)
account = Registration.to_account(registration)
profile = Registration.to_profile(registration)
MyApp.Repo.transact(fn ->
MyApp.Repo.insert_all("accounts", [account])
MyApp.Repo.insert_all("profiles", [profile])
end)
{:ok, registration}
else
# Annotate the action so the UI shows errors
changeset = %{changeset | action: :registration}
{:error, changeset}
end
Функции to_account/1 и to_profile/1 в Registration получат структуру регистрации и соответствующим образом разделят атрибуты:
def to_account(registration) do
Map.take(registration, [:email])
end
def to_profile(%{first_name: first, last_name: last}) do
%{name: "#{first} #{last}"}
end
В приведённом выше примере разделение сопоставления между базой данных и Elixir, а также между Elixir и интерфейсом делает код понятнее, а структуры данных — проще.
Обратите внимание: для непосредственного добавления данных в обе таблицы — «accounts» и «profiles» — мы использовали MyApp.Repo.insert_all/2. Мы решили вовсе обойтись без схем. Однако ничто не мешает вам также определить схемы Account и Profile и изменить to_account/1 и to_profile/1 так, чтобы они возвращали соответственно структуры %Account{} и %Profile{}. Полученные структуры можно вставить с помощью стандартной операции MyApp.Repo.insert/2. Во время вставки также можно проверить уникальность и другие ограничения, обернув структуры в набор изменений.
Наборы изменений без схем
Хотя для набора изменений мы определили схему Registration, Ecto также позволяет разработчикам использовать наборы изменений без схем. Мы можем динамически определить данные и их типы. Перепишем приведённый выше набор изменений для регистрации так, чтобы обойтись без схем:
data = %{}
types = %{name: :string, email: :string}
# The data+types tuple is equivalent to %Registration{}
changeset =
{data, types}
|> Ecto.Changeset.cast(params["sign_up"], Map.keys(types))
|> validate_required(...)
|> validate_length(...)
Этот подход можно использовать для проверки конечных точек API, поисковых форм и других источников данных. Выбор в пользу схем зависит главным образом от того, хотите ли вы использовать одно и то же сопоставление в разных местах или предпочитаете гарантии на этапе компиляции, которые дают структуры Elixir. В противном случае можно полностью обойтись без схем — как при работе с наборами изменений, так и при взаимодействии с репозиторием.
Однако самый важный урок этого руководства заключается не в том, когда следует или не следует использовать схемы, а в том, что большую задачу можно разбить на более мелкие, решаемые независимо друг от друга, и в результате получить более чистое решение. В приведённом выше примере выбор в пользу схем или отказ от них повлиял на решение гораздо меньше, чем разделение задачи регистрации на части.
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/data-mapping-and-validation.html