Spec-Zone.ru › Phoenix

Встроенные схемы

Исходный код

Встроенные схемы позволяют определять и проверять структурированные данные. Эти данные могут храниться в памяти или в базе данных. Вот несколько примеров использования встроенных схем:

  • Вы работаете с промежуточными данными, например когда поля формы пользовательского интерфейса соответствуют нескольким таблицам в базе данных, или моделируете сущности, не связанные с базой данных, например форму обратной связи

  • Вы работаете с сохранённой родительской схемой и хотите встроить в неё данные, которые...

    • просты, например карту пользовательских предпочтений в схеме 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.

← Предыдущая страница Динамические запросы
Следующая страница → Мультитенантность с префиксами запросов

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

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

© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/embedded-schemas.html

Spec-Zone.ru

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