Spec-Zone.ru › Phoenix

Сопоставление и проверка данных

Исходный код

Рассмотрим роль схем при проверке и приведении типов данных с помощью наборов изменений. Как мы увидим, иногда лучшее решение — не полностью отказываться от схем, а разбить большую схему на несколько меньших. Например, одну — для чтения данных, другую — для записи. Или одну — для базы данных, другую — для форм.

Схемы выполняют сопоставление

В документации модуля 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. В противном случае можно полностью обойтись без схем — как при работе с наборами изменений, так и при взаимодействии с репозиторием.

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

← Предыдущая страница Ограничения и операции upsert
Следующая страница → Типы длительности в Postgrex

Пакет Hex Предпросмотр Hex (текущий файл) Посмотреть llms.txt Скачать версию ePub

Создано с помощью ExDoc (v0.40.2) для языка программирования Elixir

© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/data-mapping-and-validation.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API