Встроенные схемы
Исходный кодВстроенные схемы позволяют определять и проверять структурированные данные. Эти данные могут храниться в памяти или в базе данных. Вот несколько примеров использования встроенных схем:
Вы работаете с промежуточными данными, например когда поля формы пользовательского интерфейса соответствуют нескольким таблицам в базе данных, или моделируете сущности, не связанные с базой данных, например форму обратной связи
-
Вы работаете с сохранённой родительской схемой и хотите встроить в неё данные, которые...
- просты, например карту пользовательских предпочтений в схеме User.
- часто меняются, например список изображений товаров со связанными структурированными данными в схеме Product.
- требуют сложного отслеживания и проверки, например схему Address в схеме User.
Вы используете базу данных для хранения документов и хотите взаимодействовать со встроенными документами и изменять их.
Пример профиля пользователя
Рассмотрим пример: у нас есть User, и мы хотим хранить информацию о его «профиле». Это данные, зависящие от пользовательского интерфейса и, вероятно, меняющиеся вместе с ним. Кроме того, эти данные не настолько важны, чтобы добавлять новые field в схему User, поскольку они не являются основополагающими для User. Встроенная схема — хорошее решение для таких данных.
defmodule User do
use Ecto.Schema
schema "users" do
field :full_name, :string
field :email, :string
field :avatar_url, :string
field :confirmed_at, :naive_datetime
embeds_one :profile, Profile do
field :online, :boolean
field :dark_mode, :boolean
field :visibility, Ecto.Enum, values: [:public, :private, :friends_only]
end
timestamps()
end
end
Встроенные данные
Представить встроенные данные в схеме можно двумя способами: Ecto.Schema.embeds_many/3 создаёт список встроенных данных, а Ecto.Schema.embeds_one/3 — только один экземпляр. Выбор влияет на поведение функций для работы со встроенными данными, таких как Ecto.Changeset.put_embed/4 и Ecto.Changeset.cast_embed/3, поэтому выберите вариант, наиболее подходящий для вашего случая. В нашем примере мы будем использовать Ecto.Schema.embeds_one/3, поскольку у пользователя может быть только один связанный с ним профиль.
defmodule User do
use Ecto.Schema
schema "users" do
field :full_name, :string
field :email, :string
field :avatar_url, :string
field :confirmed_at, :naive_datetime
embeds_one :profile, Profile do
field :online, :boolean
field :dark_mode, :boolean
field :visibility, Ecto.Enum, values: [:public, :private, :friends_only]
end
timestamps()
end
end
Встроенные схемы, определённые таким образом, называются определёнными inline. Это означает, что они:
- создаются как модуль в области видимости родительского модуля с соответствующей структурой (в примере выше модуль будет называться
User.Profile) - сохраняются в родительской схеме
- требуют указания параметра
withдляEcto.Changeset.cast_embed/3
Выделение встроенных схем
Хотя приведённая выше схема User проста и достаточна, может возникнуть необходимость работать со структурой встроенного профиля независимо. Например, если большая часть функциональности предназначена исключительно для работы с данными профиля, стоит выделить встроенную схему в отдельный модуль. Это можно сделать с помощью Ecto.Schema.embedded_schema/1.
# user/user.ex
defmodule User do
use Ecto.Schema
schema "users" do
field :full_name, :string
field :email, :string
field :avatar_url, :string
field :confirmed_at, :naive_datetime
embeds_one :profile, UserProfile
timestamps()
end
end
# user/user_profile.ex
defmodule UserProfile do
use Ecto.Schema
embedded_schema do
field :online, :boolean
field :dark_mode, :boolean
field :visibility, Ecto.Enum, values: [:public, :private, :friends_only]
end
end
Встроенные схемы, определённые таким образом, называются явно определёнными. Они:
- являются отдельными модулями со своей областью видимости, функциями changeset, свойствами, документацией и т. д.
- могут встраиваться в несколько родительских схем
- не зависят от способа хранения, то есть
embedded_schemaне обязательно сохранять
Важно помнить, что у embedded_schema есть множество вариантов использования, не связанных с embeds_one и embeds_many. Поскольку они не зависят от способа хранения, они идеально подходят для ситуаций, когда нужно работать со структурированными данными, не обязательно сохраняя их. Например, при создании формы обратной связи данные всё равно нужно разобрать и проверить, но, вероятно, нигде не нужно их сохранять. Вместо этого они используются для отправки письма. Встроенные схемы отлично подходят для такого случая.
Миграции
Чтобы сохранить встроенную схему в базе данных, нужно написать миграцию, добавляющую встроенные данные.
alter table("users") do
add :profile, :map
end
Независимо от того, используете ли вы embeds_one или embeds_many, рекомендуется применять тип данных :map (хотя {:array, :map} также работает с embeds_many). Это связано с тем, что реляционные базы данных обычно представляют :map в формате JSON (или JSONB в Postgres), что позволяет библиотекам адаптеров Ecto гибче и эффективнее хранить данные.
Changeset
Функциональность changeset для встроенных данных позволяет выполнять произвольные проверки данных. Для каждого модуля можно определить функцию changeset. Например, модуль UserProfile может требовать наличия полей online и visibility при создании changeset.
defmodule UserProfile do
# ...
def changeset(%UserProfile{} = profile, attrs \\ %{}) do
profile
|> cast(attrs, [:online, :dark_mode, :visibility])
|> validate_required([:online, :visibility])
end
end
profile = %UserProfile{}
UserProfile.changeset(profile, %{online: true, visibility: :public})
При этом функция changeset модуля User может выполнять собственные проверки, не заботясь о деталях изменений в UserProfile, поскольку может передать эту ответственность UserProfile с помощью cast_embed/3. Ошибка проверки во встроенных данных приведёт к тому, что родительский changeset станет недействительным, даже если в самом родительском changeset ошибок не было.
defmodule User do
# ...
def changeset(user, attrs \\ %{}) do
user
|> cast(attrs, [:full_name, :email, :avatar_url])
|> cast_embed(:profile, required: true)
end
end
changeset = User.changeset(%User{}, %{profile: %{online: true}})
changeset.valid? # => false; "visibility can't be blank"
changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}})
changeset.valid? # => true
Если вы оставили встроенную схему внутри родительского модуля, например не выделяли UserProfile в отдельный модуль, вы всё равно можете определить собственные функции changeset для встроенных данных в родительской схеме.
defmodule User do
use Ecto.Schema
schema "users" do
field :full_name, :string
field :email, :string
field :avatar_url, :string
field :confirmed_at, :naive_datetime
embeds_one :profile, Profile do
field :online, :boolean
field :dark_mode, :boolean
field :visibility, Ecto.Enum, values: [:public, :private, :friends_only]
end
timestamps()
end
def changeset(%User{} = user, attrs \\ %{}) do
user
|> cast(attrs, [:full_name, :email])
|> cast_embed(:profile, required: true, with: &profile_changeset/2)
end
def profile_changeset(profile, attrs \\ %{}) do
profile
|> cast(attrs, [:online, :dark_mode, :visibility])
|> validate_required([:online, :visibility])
end
end
changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}})
changeset.valid? # => true
Запросы к встроенным данным
После записи встроенных данных в базу данных их можно использовать в запросах к родительской схеме.
user_changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}})
{:ok, _user} = Repo.insert(user_changeset)
(Ecto.Query.from u in User, select: {u.profile["online"], u.profile["visibility"]}) |> Repo.one
# => {true, "public"}
(Ecto.Query.from u in User, select: u.profile, where: u.profile["visibility"] == ^:public) |> Repo.all
# => [
# %UserProfile{
# id: "...",
# online: true,
# dark_mode: nil,
# visibility: :public
# }
#]
В базах данных, где :map хранятся как JSONB (например, в Postgres), Ecto самостоятельно создаёт подходящие запросы jsonpath. Дополнительные примеры запросов к встроенным схемам приведены в json_extract_path/2.
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/embedded-schemas.html