Spec-Zone.ru › Phoenix

Ecto.Schema

Исходный код

Схема Ecto сопоставляет внешние данные со структурами Elixir.

Определить схему можно с помощью двух основных API: schema/2 и embedded_schema/1.

schema/2 обычно используется для сопоставления данных из постоянного источника, как правило, таблицы базы данных, со структурами Elixir и обратно с помощью модуля Ecto.Repo. Поэтому первым аргументом schema/2 является имя источника (таблицы). Структуры, определённые с помощью schema/2, также содержат поле __meta__ с метаданными, указывающими состояние структуры, например, была ли она создана, загружена или удалена. Схемы также поддерживают ассоциации с помощью таких API, как has_one/3 и belongs_to/3. Справочник по различным типам ассоциаций и их миграциям см. в шпаргалке по ассоциациям.

С другой стороны, embedded_schema/1 используется для определения схем, встроенных в другие схемы или существующих только в памяти. Например, такие схемы можно использовать для получения и проверки данных из интерфейса командной строки или контактной формы, не сохраняя их где-либо. Такие структуры не содержат поля __meta__, поскольку они никогда не сохраняются.

Обе схемы можно использовать вместе с changeset для фильтрации, приведения типов и проверки данных. Помимо работы в качестве сопоставителей данных, embedded_schema/1 и schema/2 также можно использовать вместе, чтобы отделить представление данных в приложении от их представления в базе данных.

Пример

defmodule User do
  use Ecto.Schema

  schema "users" do
    field :name, :string
    field :age, :integer, default: 0
    field :password, :string, redact: true
    has_many :posts, Post
  end
end

По умолчанию схема автоматически создаёт первичный ключ с именем id и типом :integer. Макрос field определяет в схеме поле с заданными именем и типом. has_many связывает множество публикаций со схемой пользователя. Схемы — это обычные структуры, которые можно создавать и изменять напрямую с помощью API структур Elixir:

iex> user = %User{name: "jane"}
iex> %{user | age: 30}

Однако чаще всего структуры приводят, проверяют и изменяют с помощью модуля Ecto.Changeset.

Первый аргумент schema/2 — это имя таблицы базы данных, которое не обязательно должно совпадать с именем модуля (обычно его называют именем схемы). Например, если вы работаете с устаревшей базой данных, можно указать имя таблицы (legacy_users) при определении схемы (User):

defmodule User do
  use Ecto.Schema

  schema "legacy_users" do
    # ... fields ...
  end
end

Схемы, основанные на источниках, по умолчанию доступны для запросов: их можно передавать модулям Ecto.Repo и использовать для построения запросов:

MyRepo.all(User)
MyRepo.all(from u in User, where: u.id == 13)

Затем репозиторий выполнит запрос к источнику/таблице.

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

defmodule SignUp do
  use Ecto.Schema

  embedded_schema do
    field :name, :string
    field :age, :integer
    field :email, :string
    field :accepts_conditions, :boolean
  end
end

defmodule Profile do
  use Ecto.Schema

  schema "profiles" do
    field :name
    field :age
    belongs_to :account, Account
  end
end

defmodule Account do
  use Ecto.Schema

  schema "accounts" do
    field :email
  end
end

Схему SignUp можно привести и проверить с помощью модуля Ecto.Changeset, а затем скопировать её данные в структуры Profile и Account, которые будут сохранены в базе данных с помощью Ecto.Repo. Встроенные схемы, в свою очередь, нельзя использовать непосредственно в запросах (они недоступны для запросов).

use Ecto.Schema

При вызове use Ecto.Schema выполняются следующие действия:

  • импортируются макросы Ecto.Schema schema/2 и embedded_schema/1
  • регистрируются значения по умолчанию для атрибутов модуля, которые можно переопределить, например @primary_key и @timestamps_opts
  • определяются функции для получения информации о схеме, такие как __schema__/1 и __changeset__/1
Подробнее об этом рассказано в документации модуля.

Маскирование полей

Поле, помеченное как redact: true, будет отображаться как **redacted** при просмотре изменений в Ecto.Changeset и будет исключено из вывода inspect для схемы, если только модуль схемы не помечен параметром @derive_inspect_for_redacted_fields false.

В модуле схемы, помеченном как @schema_redact :all_except_primary_keys, будут замаскированы все поля, кроме первичных ключей.

Атрибуты схемы

Поддерживаемые атрибуты для настройки определяемой схемы. Их необходимо задать после вызова use Ecto.Schema и до определения schema/2.

Доступны следующие атрибуты:

  • @primary_key — настраивает первичный ключ схемы. Ожидается кортеж {field_name, type, options} с именем поля первичного ключа, типом (обычно :id или :binary_id, но может быть любым типом) и параметрами. Также принимает false, чтобы отключить создание поля первичного ключа. По умолчанию используется {:id, :id, autogenerate: true}.

  • @schema_prefix — настраивает префикс схемы. По умолчанию используется nil, поэтому структуры и запросы создаются без префикса. Если задано значение, префикс будет использоваться для каждой создаваемой структуры и в запросах при использовании схемы в from или join. В PostgreSQL префикс называется «SCHEMA» (обычно задаётся через search_path в Postgres). В MySQL префикс указывает на базы данных.

  • @schema_context — настраивает контекст схемы. По умолчанию используется nil, поэтому структуры и запросы создаются без контекста. Встроенные SQL-адаптеры не используют контекст.

  • @schema_redact — если задано значение :all_except_primary_keys, Ecto будет считать все поля, кроме первичных ключей, помеченными как маскируемые. По умолчанию используется false, так как по умолчанию ни одно поле не маскируется. Это значение можно изменить отдельно для каждого поля с помощью параметра :redact.

  • @foreign_key_type — настраивает используемый по умолчанию тип внешнего ключа для ассоциаций belongs_to. Его необходимо задать в том же модуле, где определён belongs_to. По умолчанию используется :id;

  • @timestamps_opts — настраивает используемый по умолчанию тип меток времени для timestamps. По умолчанию используется [type: :naive_datetime];

  • @derive — аналог @derive, доступного в Kernel.defstruct/1, поскольку схема неявно определяет структуру;

  • @derive_inspect_for_redacted_fields false — Ecto автоматически реализует протокол Inspect, если заданы маскируемые поля. Этот параметр отключает такое поведение;

  • @field_source_mapper — функция, которая получает имя текущего поля и возвращает соответствие этого имени полю в базовом источнике. Иными словами, это механизм автоматического создания параметра :source для макроса field. По умолчанию используется fn x -> x end, поэтому преобразование имён полей не выполняется;

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

Например, если ваша база данных не поддерживает автоматически увеличиваемые первичные ключи и требует использовать UUID или RecordID, можно настроить и использовать :binary_id в качестве типа первичного ключа следующим образом:

# Define a module to be used as base
defmodule MyApp.Schema do
  defmacro __using__(_) do
    quote do
      use Ecto.Schema
      @primary_key {:id, :binary_id, autogenerate: true}
      @foreign_key_type :binary_id
    end
  end
end

# Now use MyApp.Schema to define new schemas
defmodule MyApp.Comment do
  use MyApp.Schema

  schema "comments" do
    belongs_to :post, MyApp.Post
  end
end

Все схемы, использующие MyApp.Schema, получат поле :id типа :binary_id в качестве первичного ключа. В следующем разделе объясняется, что представляет собой тип :binary_id.

Ассоциация belongs_to в MyApp.Comment также определит поле :post_id типа :binary_id, ссылающееся на поле :id схемы MyApp.Post.

Первичные ключи

Ecto поддерживает два типа идентификаторов — :id и :binary_id, которые часто используются в качестве типов первичных ключей и ассоциаций.

Тип :id используется, когда первичный ключ является целым числом, а :binary_id — для первичных ключей в определённых бинарных форматах. Это может быть Ecto.UUID для таких баз данных, как PostgreSQL и MySQL, или специальный ObjectID либо RecordID, часто используемый в базах данных NoSQL.

В обоих случаях семантика этих типов определяется базовым адаптером/базой данных. Если использовать тип :id с :autogenerate, это означает, что за автоматическую генерацию идентификатора отвечает база данных. Так часто устроены первичные ключи реляционных баз данных, где их значения увеличиваются автоматически.

В Ecto есть два способа определить первичные ключи: использовать атрибут модуля @primary_key или параметр primary_key: true для field/3 в определении схемы. Эти способы не являются взаимоисключающими и могут использоваться вместе.

Для первичных ключей из одного поля и совместного использования определений первичных ключей несколькими схемами с помощью макросов предпочтительно использовать @primary_key. Установка @primary_key также автоматически настраивает типы ссылок для ассоциаций has_one и has_many.

Ecto также поддерживает составные первичные ключи. В этом случае для полей схемы нужно использовать primary_key: true. Обычно вместе с этим устанавливают @primary_key false, чтобы отключить создание дополнительных полей первичного ключа.

Помимо :id и :binary_id, которые часто используются для первичных и внешних ключей, Ecto предлагает множество типов для любых полей.

Типы и приведение типов

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

Примитивные типы

Примитивные типы:

Тип Ecto Тип Elixir Литеральный синтаксис в запросе
:id integer 1, 2, 3
:binary_id binary <<int, int, int, ...>>
:integer integer 1, 2, 3
:float float 1.0, 2.0, 3.0
:boolean boolean true, false
:string строка в кодировке UTF-8 string "hello"
:binary binary <<int, int, int, ...>>
:bitstring bitstring <<_::size>>
{:array, inner_type} list [value, value, value, ...]
:map map
{:map, inner_type} map
:decimal Decimal
:date Date
:time Time
:time_usec Time
:naive_datetime NaiveDateTime
:naive_datetime_usec NaiveDateTime
:utc_datetime DateTime
:utc_datetime_usec DateTime
:duration Duration

Примечания:

  • При использовании миграций базы данных, предоставляемых «Ecto SQL», в качестве типа столбца можно указать тип Ecto. Однако учтите, что один и тот же тип Ecto может поддерживать несколько типов баз данных. Например, все :varchar, :text, :bytea и т. д. преобразуются в :string в Ecto. Аналогично, :decimal в Ecto можно использовать для :numeric и других типов баз данных. Подробнее см. в разделе все типы миграций.

  • Для типов {:array, inner_type} и {:map, inner_type} замените inner_type одним из допустимых типов, например :string.

  • Для типа :decimal значения +Infinity, -Infinity и NaN не поддерживаются, хотя библиотека Decimal умеет с ними работать. Чтобы поддержать их, можно создать пользовательский тип.

  • Для календарных типов с микросекундами и без них точность проверяется при сохранении в БД. Например, приведение ~T[09:00:00] к :time_usec будет успешным и даст результат ~T[09:00:00.000000], но сохранение значения типа без микросекунд как :time_usec завершится ошибкой. Аналогично, приведение ~T[09:00:00.000000] к :time будет успешным, но сохранение завершится ошибкой. Это поведение аналогично поведению других типов: приведение необходимо выполнять явно, оно никогда не выполняется неявно при загрузке из базы данных или записи в неё.

  • Для типа :duration может потребоваться включить поддержку Duration в адаптере. Инструкции по её включению в Postgrex см. на странице HexDocs.

Пользовательские типы

Помимо примитивных типов, Ecto позволяет разработчикам реализовывать пользовательские типы, расширяя поведение Ecto.

Пользовательский тип — это модуль, реализующий один из поведений Ecto.Type или Ecto.ParameterizedType. По умолчанию Ecto предоставляет следующие пользовательские типы:

Пользовательский тип Тип базы данных Тип Elixir
Ecto.UUID :uuid (в виде двоичных данных) string() (в виде UUID)
Ecto.Enum :string atom()

Кроме того, в схемах можно создавать виртуальные поля, передав параметр virtual: true. Эти поля не сохраняются в базе данных. При необходимости для них можно отключить проверку типа, объявив тип :any.

Типы даты и времени

Доступны четыре примитивных типа даты и времени:

  • naive_datetime — точность до секунд; значения приводятся к структуре NaiveDateTime в Elixir, в которой отсутствуют сведения о часовом поясе.

  • naive_datetime_usec — точность по умолчанию составляет микросекунды; значения также приводятся к NaiveDateTime без сведений о часовом поясе.

  • utc_datetime — точность до секунд; значения приводятся к структуре DateTime в Elixir, а часовой пояс должен быть установлен в UTC.

  • utc_datetime_usec — точность по умолчанию составляет микросекунды; значения также приводятся к DateTime, часовой пояс которого должен быть установлен в UTC.

В хранилище данных все эти типы представлены одной и той же временной меткой или значением даты и времени; они различаются точностью и способом загрузки данных в Elixir.

Различная точность позволяет разработчикам выбирать тип, совместимый с базой данных и требованиями проекта к точности. Например, некоторые старые версии MySQL не поддерживают микросекунды в полях даты и времени.

Выбирая тип даты и времени, помните, что функции Elixir, такие как NaiveDateTime.utc_now/0, по умолчанию используют точность 6. Приведение значения с точностью больше 0 к типу, отличному от usec, отбросит все микросекунды и установит точность 0.

Тип map

Тип map позволяет разработчикам напрямую сохранять карту Elixir в базе данных:

# In your migration
create table(:users) do
  add :data, :map
end

# In your schema
field :data, :map

# Now in your code
user = Repo.insert! %User{data: %{"foo" => "bar"}}

Мы рекомендуем использовать в качестве ключей карты строки или целые числа, а не атомы. Атомы могут приниматься в зависимости от способа сериализации карт, но из соображений безопасности база данных всегда преобразует ключи-атомы в строки.

Для поддержки карт разные базы данных могут использовать разные механизмы. Например, PostgreSQL хранит такие значения в полях jsonb, что позволяет запрашивать отдельные их части. MSSQL, с другой стороны, пока не предоставляет тип JSON, поэтому значение хранится в текстовом поле.

Для работы с картами в таких базах данных Ecto потребуется библиотека JSON. По умолчанию Ecto использует Jason; её необходимо добавить в deps в mix.exs:

{:jason, "~> 1.0"}

Однако можно настроить адаптер на использование другой библиотеки. Например, для Postgres:

config :postgrex, :json_library, YourLibraryOfChoice

Или для MySQL:

config :myxql, :json_library, YourLibraryOfChoice

Если вы меняете библиотеку JSON, не забудьте после этого пересобрать адаптер, очистив текущую сборку:

mix deps.clean --build postgrex

Приведение типов

При непосредственном изменении структуры разработчик должен самостоятельно убедиться, что значения полей имеют правильный тип. Например, можно создать структуру пользователя с недопустимым значением для age:

iex> user = %User{age: "0"}
iex> user.age
"0"

Однако при попытке сохранить приведённую выше структуру возникнет ошибка, поскольку Ecto проверяет типы при передаче данных адаптеру или базе данных.

Поэтому при работе с внешними данными и их обработке рекомендуется использовать Ecto.Changeset, которые позволяют отфильтровать внешние данные и корректно привести их типы:

changeset = Ecto.Changeset.cast(%User{}, %{"age" => "0"}, [:age])
user = Repo.insert!(changeset)

С помощью схем и changeset в Ecto можно приводить к нужному типу и проверять любые данные, независимо от того, будут ли они сохранены в репозитории Ecto.

Интроспекция

Для любого модуля схемы будет сгенерирована функция __schema__, которую можно использовать для интроспекции схемы во время выполнения:

  • __schema__(:source) — возвращает источник, переданный в schema/2;

  • __schema__(:prefix) — возвращает необязательный префикс источника, заданный атрибутом схемы @schema_prefix;

  • __schema__(:primary_key) — возвращает список полей первичного ключа (пустой, если таких полей нет);

  • __schema__(:fields) — возвращает список имён всех невиртуальных полей;

  • __schema__(:virtual_fields) — возвращает список имён всех виртуальных полей;

  • __schema__(:field_source, field) — возвращает псевдоним указанного поля;

  • __schema__(:type, field) — возвращает тип указанного невиртуального поля;

  • __schema__(:virtual_type, field) — возвращает тип указанного виртуального поля;

  • __schema__(:associations) — возвращает список имён всех полей ассоциаций;

  • __schema__(:association, assoc) — возвращает сведения об ассоциации для указанной ассоциации;

  • __schema__(:embeds) — возвращает список имён всех встроенных полей;

  • __schema__(:embed, embed) — возвращает сведения о встраивании для указанного встроенного поля;

  • __schema__(:read_after_writes) — невиртуальные поля, которые необходимо считывать из базы данных после каждой записи (вставки, обновления и удаления);

  • __schema__(:autogenerate_id) — первичный ключ, генерируемый автоматически при вставке;

  • __schema__(:autogenerate_fields) — возвращает список имён полей, генерируемых автоматически при вставке, за исключением первичного ключа;

  • __schema__(:redact_fields) — возвращает список имён скрытых полей;

Кроме того, определены обе функции __struct__ и __changeset__, поэтому доступны функциональность структур и changeset.

Функция __schema__ может принимать и другие значения, однако они не являются частью публичного API. Любые значения, отсутствующие в списке выше, могут быть изменены в любое время без предупреждения.

Работа со спецификациями типов

Генерация спецификаций типов для схем выходит за рамки Ecto.Schema.

Чтобы использовать такие типы, как User.t(), необходимо вручную определить t/0:

defmodule User do
  use Ecto.Schema

  @type t :: %__MODULE__{
    name: String.t(),
    age: non_neg_integer()
  }

  # ... schema ...
end

Указывать тип каждого поля необязательно, но желательно.

Краткое описание

Типы

belongs_to(t)
embedded_schema()
embeds_many(t)
embeds_one(t)
has_many(t)
has_one(t)
many_to_many(t)
prefix()
schema()
source()
t()

Функции

belongs_to(name, schema, opts \\ [])

Обозначает связь «один к одному» или «многие к одному» с другой схемой.

embedded_schema(list)

Определяет встроенную схему с указанными определениями полей.

embeds_many(name, schema, opts \\ [])

Обозначает встраивание нескольких схем.

embeds_many(name, schema, opts, list)

Обозначает встраивание нескольких схем.

embeds_one(name, schema, opts \\ [])

Обозначает встраивание схемы.

embeds_one(name, schema, opts, list)

Обозначает встраивание схемы.

field(name, type \\ :string, opts \\ [])

Определяет поле схемы с указанными именем и типом.

has_many(name, schema, opts \\ [])

Обозначает связь «один ко многим» с другой схемой.

has_one(name, schema, opts \\ [])

Обозначает связь «один к одному» с другой схемой.

many_to_many(name, schema, opts \\ [])

Обозначает связь «многие ко многим» с другой схемой.

schema(source, list)

Определяет структуру схемы с именем источника и определениями полей.

timestamps(opts \\ [])

Создаёт поля временных меток :inserted_at и :updated_at.

Типы

belongs_to(t)Источник

@type belongs_to(t) :: t | Ecto.Association.NotLoaded.t()

embedded_schema()Источник

@type embedded_schema() :: %{optional(atom()) => any(), __struct__: atom()}

embeds_many(t)Источник

@type embeds_many(t) :: [t]

embeds_one(t)Источник

@type embeds_one(t) :: t

has_many(t)Источник

@type has_many(t) :: [t] | Ecto.Association.NotLoaded.t()

has_one(t)Источник

@type has_one(t) :: t | Ecto.Association.NotLoaded.t()

many_to_many(t)Источник

@type many_to_many(t) :: [t] | Ecto.Association.NotLoaded.t()

prefix()Источник

@type prefix() :: any()

schema()Источник

@type schema() :: %{
  optional(atom()) => any(),
  __struct__: atom(),
  __meta__: Ecto.Schema.Metadata.t()
}

source()Источник

@type source() :: String.t()

t()Источник

@type t() :: schema() | embedded_schema()

Функции

belongs_to(name, schema, opts \\ [])Источник

Обозначает связь «один к одному» или «многие к одному» с другой схемой.

Текущая схема принадлежит нулю или одной записи другой схемы. В другой схеме часто есть поле has_one или has_many с обратной связью.

Следует использовать belongs_to в таблице, содержащей внешний ключ. Представьте связь «компания <-> сотрудник». Если в таблице сотрудника в базовой базе данных содержится company_id, мы говорим, что сотрудник принадлежит компании.

Фактически, при вызове этого макроса в схеме автоматически определяется поле с именем внешнего ключа.

Параметры

  • :foreign_key — задаёт имя поля внешнего ключа; по умолчанию используется имя связи с суффиксом _id. Например, belongs_to :company определит внешний ключ :company_id. Для связанного поля has_one или has_many в другой схеме также следует установить параметр :foreign_key с тем же значением.

  • :references — задаёт ключ в другой схеме, используемый для связи; по умолчанию: :id

  • :define_field — если значение равно false, поле :foreign_key не определяется автоматически, предполагая, что пользователь определяет это поле вручную в другом месте

  • :type — задаёт тип автоматически определяемого :foreign_key. По умолчанию: :integer; значение можно задать для каждой схемы с помощью @foreign_key_type

  • :on_replace — действие для связей при замене записи во время приведения типов или изменения родительского changeset. Возможны значения :raise (по умолчанию), :mark_as_invalid, :nilify, :update или :delete. Подробнее см. раздел о связанных данных в Ecto.Changeset.

  • :defaults — значения по умолчанию для построения связи. Это может быть список ключевых слов с параметрами, переопределяющими схему связи, или atom/{module, function, args}, принимающий структуру связи и структуру-владельца в качестве аргументов. Например, если задать Comment.belongs_to :post, defaults: [public: true], то при использовании Ecto.build_assoc(comment, :post) у публикации будет post.public == true. Также можно задать Comment.belongs_to :post, defaults: :update_post, что вызовет Comment.update_post(post, comment), или кортеж MFA, например {Mod, fun, [arg3, arg4]}, что вызовет Mod.fun(post, comment, arg3, arg4)

  • :primary_key — если базовое поле belongs_to является первичным ключом

  • :source — задаёт имя, используемое для этого поля в базе данных

  • :where — фильтр для связи. См. раздел «Фильтрация связей» в has_many/3.

Примеры

defmodule Comment do
  use Ecto.Schema

  schema "comments" do
    belongs_to :post, Post
  end
end

# The post can come preloaded on the comment record
[comment] = Repo.all(from(c in Comment, where: c.id == 42, preload: :post))
comment.post #=> %Post{...}

Если для базового поля нужны собственные параметры, его можно определить явно, а затем передать define_field: false в belongs_to:

defmodule Comment do
  use Ecto.Schema

  schema "comments" do
    field :post_id, :integer, ... # custom options
    belongs_to :post, Post, define_field: false
  end
end

При использовании EctoSQL таблица comments должна содержать столбец post_id, ссылающийся на таблицу posts. В миграциях это можно сделать так:

add :post_id,
    references(:posts, on_delete: :delete_all),
    null: false

Дополнительные примеры см. в шпаргалке по связям.

Полиморфные связи

Один из распространённых вариантов использования связей belongs_to — реализация полиморфизма. Например, представьте, что у вас есть схема Comment, которую вы хотите использовать для комментирования как задач, так и публикаций.

Некоторые абстракции вынуждают определять в базе данных особую полиморфную связь с двумя полями:

* commentable_type
* commentable_id

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

В Ecto есть три способа решить эту проблему. Самый простой — определить в схеме Comment несколько полей, по одному для каждой связи:

* task_id
* post_id

Если у вас не десятки столбцов, для разработчика это проще, лучше подходит для баз данных и эффективнее во всех отношениях.

Другой вариант: поскольку Ecto не привязывает схему к конкретной таблице, для каждой связи можно использовать отдельную таблицу. Начнём заново и определим новую схему Comment:

defmodule Comment do
  use Ecto.Schema

  schema "abstract table: comments" do
    # This will be used by associations on each "concrete" table
    field :assoc_id, :integer
  end
end

Обратите внимание, что имя таблицы изменено на «абстрактная таблица: comments». Вы можете выбрать любое имя; важно то, что этой конкретной таблицы никогда не будет.

Теперь в схемах Post и Task:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    has_many :comments, {"posts_comments", Comment}, foreign_key: :assoc_id
  end
end

defmodule Task do
  use Ecto.Schema

  schema "tasks" do
    has_many :comments, {"tasks_comments", Comment}, foreign_key: :assoc_id
  end
end

Теперь для каждой связи используется собственная таблица — «posts_comments» и «tasks_comments», которые необходимо создать в миграциях. Преимущество такого подхода в том, что несвязанные данные никогда не хранятся вместе, а ссылки в базе данных остаются быстрыми и корректными.

Единственное ограничение этого метода — нельзя создавать комментарии напрямую. Например, следующая команда

Repo.insert!(%Comment{})

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

Repo.insert!(build_assoc(post, :comments))

с помощью функции Ecto.build_assoc/3. Можно также использовать Ecto.assoc/2 или передать кортеж в синтаксисе запроса, чтобы легко получить связанные комментарии для заданной публикации или задачи:

# Fetch all comments associated with the given task
Repo.all(Ecto.assoc(task, :comments))

Или все комментарии в заданной таблице:

Repo.all from(c in {"posts_comments", Comment}), ...)

Третий и последний вариант — использовать many_to_many/3 для определения связей между ресурсами. В этом случае в таблице comments не будет внешнего ключа; вместо этого записи будут связываться через промежуточную таблицу:

defmodule Comment do
  use Ecto.Schema
  schema "comments" do
    # ...
  end
end

В схемах публикаций и задач:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    many_to_many :comments, Comment, join_through: "posts_comments"
  end
end

defmodule Task do
  use Ecto.Schema

  schema "tasks" do
    many_to_many :comments, Comment, join_through: "tasks_comments"
  end
end

Подробнее об этом подходе см. в разделе many_to_many/3.

embedded_schema(list)Источник

Определяет встроенную схему с заданными определениями полей.

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

Для встроенных схем по умолчанию задаётся тип первичного ключа :binary_id, но его можно настроить с помощью атрибута @primary_key.

Связи belongs_to/3 можно определять внутри встроенных схем. Однако любая связь, вложенная во встроенную схему, не будет сохраняться в базе данных при вызове Ecto.Repo.insert/2 или Ecto.Repo.update/2.

embeds_many(name, schema, opts \\ [])Источник

Обозначает встраивание нескольких схем.

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

Рекомендуется объявлять поле embeds_many/3 с типом :map в миграциях, а не использовать {:array, :map}. Ecto может работать как с картами, так и с массивами в качестве контейнеров для встроенных схем (в большинстве баз данных карты представлены в формате JSON, что позволяет Ecto выбрать наиболее подходящий вариант).

У встроенной схемы может быть первичный ключ, а может и не быть. Ecto использует первичные ключи, чтобы определить, обновляется ли встроенная схема. Если первичного ключа нет, но вы всё равно хотите обновлять список встроенных схем, необходимо установить :on_replace в :delete; это приведёт к удалению всех текущих встроенных схем и их замене новыми при каждом задании нового списка.

Информацию о кодировании и декодировании встроенных схем см. в документации к embeds_one/3.

Параметры

  • :on_replace — действие для связей при замене встроенной схемы во время приведения типов или изменения родительского changeset. Возможны значения :raise (по умолчанию), :mark_as_invalid или :delete. Подробнее см. раздел о связанных данных в Ecto.Changeset.

  • :source — задаёт имя, используемое для этого поля в базе данных. Полезно при подключении к существующей базе данных. Значение должно быть атомом.

  • :load_in_query — если значение равно false, поле не будет загружаться при выборе всей структуры в запросе, например from p in Post, select: p. По умолчанию — true.

Примеры

defmodule Order do
  use Ecto.Schema

  schema "orders" do
    embeds_many :items, Item
  end
end

defmodule Item do
  use Ecto.Schema

  embedded_schema do
    field :title
  end
end

# The items are loaded with the order
order = Repo.get!(Order, 42)
order.items #=> [%Item{...}, ...]

Добавлять и удалять встроенные схемы можно только через API Ecto.Changeset, чтобы Ecto мог правильно отслеживать жизненный цикл встроенных схем:

# Order has no items
order = Repo.get!(Order, 42)
order.items
# => []

items  = [%Item{title: "Soap"}]

# Generate a changeset
changeset = Ecto.Changeset.change(order)

# Put a one or more new items
changeset = Ecto.Changeset.put_embed(changeset, :items, items)

# Update the order and fetch items
items = Repo.update!(changeset).items

# Items are generated with a unique identification
items
# => [%Item{id: "20a97d94-f79b-4e63-a875-85deed7719b7", title: "Soap"}]

Для обновления встроенных схем необходимо использовать changeset для каждой изменённой встроенной схемы.

# Order has an existing items
order = Repo.get!(Order, 42)
order.items
# => [%Item{id: "20a97d94-f79b-4e63-a875-85deed7719b7", title: "Soap"}]

# Generate a changeset
changeset = Ecto.Changeset.change(order)

# Put the updated item as a changeset
current_item = List.first(order.items)
item_changeset = Ecto.Changeset.change(current_item, title: "Mujju's Soap")
order_changeset = Ecto.Changeset.put_embed(changeset, :items, [item_changeset])

# Update the order and fetch items
items = Repo.update!(order_changeset).items

# Item has the updated title
items
# => [%Item{id: "20a97d94-f79b-4e63-a875-85deed7719b7", title: "Mujju's Soap"}]

Встроенная схема, определённая непосредственно

В простых случаях модуль схемы можно определить непосредственно в родительской схеме:

defmodule Parent do
  use Ecto.Schema

  schema "parents" do
    field :name, :string

    embeds_many :children, Child do
      field :name, :string
      field :age,  :integer
    end
  end
end

Для встроенных схем также автоматически настраиваются первичные ключи; по умолчанию используется {:id, :binary_id, autogenerate: true}. Настроить его можно, передав параметр :primary_key с теми же аргументами, что и у @primary_key (подробнее см. раздел «Атрибуты схемы»).

При таком определении встроенной схемы будет создан модуль Parent.Child с соответствующей структурой. Чтобы правильно привести типы встроенной схемы, при приведении типов встроенных схем, определённых непосредственно, необходимо использовать параметр :with в cast_embed/3, чтобы передать подходящую функцию приведения типов. Например:

def changeset(schema, params) do
  schema
  |> cast(params, [:name])
  |> cast_embed(:children, with: &child_changeset/2)
end

defp child_changeset(schema, params) do
  schema
  |> cast(params, [:name, :age])
end

embeds_many(name, schema, opts, list)Источник

Обозначает встраивание нескольких схем.

Параметры и примеры см. в документации к embeds_many/3.

embeds_one(name, schema, opts \\ [])Источник

Обозначает встраивание схемы.

Текущая схема содержит ноль или одну запись другой схемы. Для хранения используется поле, аналогичное типу :map, но встраиваемые схемы поддерживают все возможности обычной схемы.

На уровне базы данных необходимо объявить поле embeds_one/3 с типом :map.

Встроенная схема может иметь первичный ключ, а может и не иметь. Ecto использует первичные ключи, чтобы определить, обновляется ли встроенная схема. Если первичного ключа нет, :on_replace следует задать значение :update или :delete, если требуется обновлять или удалять текущую встроенную схему при установке новой.

Параметры

  • :primary_key — параметр :primary_key можно использовать с теми же аргументами, что и @primary_key (подробности см. в разделе Атрибуты схемы). Для встроенных схем также автоматически создаются первичные ключи, по умолчанию {:id, :binary_id, autogenerate: true}. Это создаст UUID v4 по умолчанию. Вместо него можно использовать UUID v7, задав первичный ключ как {:id, Ecto.UUID, autogenerate: [version: 7]}. Обратите внимание: :primary_key не считываются автоматически при insert/2, если не задан один из параметров autogenerate: true или read_after_writes: true.

  • :on_replace — действие, выполняемое со связями при замене встроенной схемы во время приведения типов или изменения changeset родительской схемы. Возможные значения: :raise (по умолчанию), :mark_as_invalid, :update или :delete. Подробности см. в разделе о связанных данных в документации Ecto.Changeset.

  • :source — задаёт имя, используемое для этого поля в базе данных. Это полезно при подключении к существующей базе данных. Значение должно быть атомом.

  • :load_in_query — если значение равно false, поле не будет загружаться при выборе всей структуры в запросе, например from p in Post, select: p. По умолчанию — true.

  • :defaults_to_struct — если значение равно true, вместо nil для поля будет использоваться инициализированная структура, как при вызове %Order.Item{}. Важно: если при загрузке схемы в исходных данных явно указано nil, поле всё равно будет загружено как nil, подобно тому, как работает :default для полей. По умолчанию — false.

Примеры

defmodule Order do
  use Ecto.Schema

  schema "orders" do
    embeds_one :item, Item
  end
end

defmodule Item do
  use Ecto.Schema

  embedded_schema do
    field :title
  end
end

# The item is loaded with the order
order = Repo.get!(Order, 42)
order.item #=> %Item{...}

Добавлять и удалять встроенные схемы можно только через API Ecto.Changeset, чтобы Ecto мог правильно отслеживать жизненный цикл встроенной схемы:

order = Repo.get!(Order, 42)
item  = %Item{title: "Soap"}

# Generate a changeset
changeset = Ecto.Changeset.change(order)

# Put a new embed to the changeset
changeset = Ecto.Changeset.put_embed(changeset, :item, item)

# Update the order, and fetch the item
item = Repo.update!(changeset).item

# Item is generated with a unique identification
item
# => %Item{id: "20a97d94-f79b-4e63-a875-85deed7719b7", title: "Soap"}

Встроенная схема, определённая прямо в родительской схеме

В простых случаях модуль схемы можно определить непосредственно в родительской схеме:

defmodule Parent do
  use Ecto.Schema

  schema "parents" do
    field :name, :string

    embeds_one :child, Child do
      field :name, :string
      field :age,  :integer
    end
  end
end

Параметры следует передавать перед блоком do, например:

embeds_one :child, Child, on_replace: :delete, primary_key: false do
  field :name, :string
  field :age,  :integer
end

Такое определение встроенной схемы создаст модуль Parent.Child с соответствующей структурой. Чтобы правильно привести типы встроенной схемы, при приведении типов встроенных схем, определённых прямо в родительской схеме, необходимо использовать параметр :with функции Ecto.Changeset.cast_embed/3, указав функцию для приведения типов. Например:

def changeset(schema, params) do
  schema
  |> cast(params, [:name])
  |> cast_embed(:child, with: &child_changeset/2)
end

defp child_changeset(schema, params) do
  schema
  |> cast(params, [:name, :age])
end

Кодирование и декодирование

Поскольку многие базы данных не поддерживают прямое кодирование и декодирование встроенных схем, Ecto часто эмулирует их с помощью специальных правил кодирования и декодирования.

Например, PostgreSQL хранит встроенные схемы в столбцах JSONB. Это означает, что типы во встроенных схемах проходят не обычный цикл dump->DB->load, а цикл encode->DB->decode->cast. Поэтому при использовании встроенных схем с такими базами данных, как PG или MySQL, убедитесь, что все ваши типы правильно кодируются и декодируются в формате JSON. Ecto гарантирует это для всех встроенных типов.

При декодировании ключи, присутствующие в базе данных, но не определённые в схеме, игнорируются. Если поле есть в схеме, но отсутствует в базе данных, его значение будет nil.

embeds_one(name, schema, opts, list)Источник

Обозначает встраивание схемы.

Параметры и примеры см. в документации для embeds_one/3.

field(name, type \\ :string, opts \\ [])Источник

Определяет поле схемы с указанными именем и типом.

Имя поля используется без изменений для чтения и записи в базу данных всеми встроенными адаптерами, если не переопределено параметром :source.

Параметры

  • :default — задаёт значение по умолчанию для схемы и структуры.

    Значение по умолчанию вычисляется во время компиляции, поэтому не используйте такие выражения, как DateTime.utc_now или Ecto.UUID.generate, иначе они будут одинаковыми для всех записей. В этом случае можно использовать параметр :autogenerate, чтобы генерировать значение во время вставки.

    Значение по умолчанию проверяется на соответствие типу поля во время компиляции. При несовпадении типов будет вызвана ошибка ArgumentError. Если тип поля невозможно определить во время компиляции, можно использовать параметр :skip_default_validation для этого поля, чтобы пропустить проверки.

    После установки значения по умолчанию, если вы передадите в changeset изменения, содержащие то же значение, что и значение по умолчанию, проверки выполняться не будут, поскольку фактически изменений нет.

  • :source — задаёт имя, используемое для этого поля в базе данных. Это полезно при подключении к существующей базе данных. Значение должно быть атомом. Это преобразование выполняется в последний момент перед отправкой запроса в базу данных. Во всём коде Elixir по-прежнему необходимо обращаться к полю по его имени, например в качестве внешнего ключа связи.

  • :autogenerate — кортеж {module, function, args} с функцией, которая вызывается перед вставкой для генерации значения поля, если оно не задано. Список параметров передаётся первым аргументом {type, :autogenerate, [options]}. Сокращённая запись true эквивалентна {type, :autogenerate, []}.

  • :read_after_writes — если значение равно true, поле всегда считывается из базы данных после вставки, обновления и удаления.

    Для реляционных баз данных это означает, что в этих запросах используется параметр RETURNING. Поэтому MySQL не поддерживает этот параметр и вызовет ошибку, если схема вставляется или обновляется с полями, считываемыми после записи.

  • :virtual — если значение равно true, поле не сохраняется в базе данных. Обратите внимание: виртуальные поля не поддерживают :autogenerate и :read_after_writes.

  • :primary_key — если значение равно true, поле входит в состав составного первичного ключа.

  • :load_in_query — если значение равно false, поле не будет загружаться при выборе всей структуры в запросе, например from p in Post, select: p. По умолчанию — true.

  • :redact — если значение равно true, при просмотре изменений в Ecto.Changeset вместо значения будет отображаться **redacted**, а поле будет исключено из вывода схемы. По умолчанию — false.

  • :skip_default_validation — если значение равно true, проверка типа во время компиляции пропускается.

  • :writable — определяет, когда поле разрешено изменять. Допустимые значения: :always, :insert или :never. Если задано значение :always, поле можно изменять при любой операции с репозиторием. Если задано значение :insert, поле можно вставить, но в дальнейшем его нельзя изменять, даже при выполнении upsert. Если задано значение :never, поле становится доступным только для чтения. По умолчанию — :always.

has_many(name, schema, opts \\ [])Источник

Обозначает связь «один ко многим» с другой схемой.

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

Параметры

  • :foreign_key - задаёт внешний ключ. Он должен соответствовать полю в другой схеме; по умолчанию используется имя текущей схемы в нижнем регистре с подчёркиваниями, дополненное суффиксом _id

  • :references - задаёт ключ в текущей схеме, который будет использоваться для связи; по умолчанию используется первичный ключ схемы

  • :through - позволяет определять эту связь на основе существующих связей. Дополнительные сведения см. в разделе о связях :through

  • :on_delete - действие, выполняемое со связями при удалении родительской записи. Возможны значения :nothing (по умолчанию), :nilify_all и :delete_all. Для большинства реляционных баз данных ИСПОЛЬЗОВАТЬ этот параметр НЕ РЕКОМЕНДУЕТСЯ. Вместо этого задайте в миграции references(:parent_id, on_delete: :delete_all). В отличие от параметра миграции, этот параметр не гарантирует целостность данных и срабатывает только при вызове Ecto.Repo.delete/2 (но не при вызове Ecto.Repo.delete_all/2); каскадное удаление не выполняется. Если у публикаций есть комментарии, а у комментариев — теги, при удалении публикации удалятся только комментарии. Если ваша база данных не поддерживает ссылки, каскадное удаление можно реализовать вручную с помощью Ecto.Multi или Ecto.Changeset.prepare_changes/2.

  • :on_replace - действие, выполняемое со связями, когда запись заменяется при приведении типов или изменении родительского changeset. Возможны значения :raise (по умолчанию), :mark_as_invalid, :nilify, :delete или :delete_if_exists. Дополнительные сведения см. в разделе о :on_replace в документации Ecto.Changeset.

  • :defaults - значения по умолчанию, используемые при создании связи. Это может быть список ключевых слов с параметрами, переопределяющими схему связи, или atom/{module, function, args}, принимающий структуру связи и структуру владельца в качестве аргументов. Например, если задать Post.has_many :comments, defaults: [public: true], то при использовании Ecto.build_assoc(post, :comments) комментарий будет содержать comment.public == true. Также можно задать Post.has_many :comments, defaults: :update_comment, чтобы вызвать Post.update_comment(comment, post), или указать кортеж MFA, например {Mod, fun, [arg3, arg4]}, чтобы вызвать Mod.fun(comment, post, arg3, arg4)

  • :where - фильтр для связи. См. раздел «Фильтрация связей» ниже. Не применяется к связям :through.

  • :preload_order - задаёт значение order_by по умолчанию при предварительной загрузке связи. Это может быть список ключевых слов/список полей или кортеж MFA, например {Mod, fun, []}. В обоих случаях результат должен быть корректным выражением order_by. Например, если задать Post.has_many :comments, preload_order: [asc: :content], то при каждой предварительной загрузке связей :comments комментарии будут упорядочены по полю :content. Обратите внимание: если вы предоставите пользовательский запрос с собственной секцией order_by, пользовательская сортировка будет иметь приоритет, а :preload_order применяться не будет. Подробнее о выражениях сортировки см. в документации Ecto.Query.order_by/3.

Примеры

defmodule Post do
  use Ecto.Schema
  schema "posts" do
    has_many :comments, Comment
  end
end

# Get all comments for a given post
post = Repo.get(Post, 42)
comments = Repo.all assoc(post, :comments)

# The comments can come preloaded on the post struct
[post] = Repo.all(from(p in Post, where: p.id == 42, preload: :comments))
post.comments #=> [%Comment{...}, ...]

При использовании EctoSQL внешний ключ следует определить в таблице comments, как показано в примерах belongs_to/3. Дополнительные примеры приведены в справочнике по связям.

has_many можно использовать для определения иерархических связей в рамках одной схемы, например для веток комментариев.

defmodule Comment do
  use Ecto.Schema
  schema "comments" do
    field :content, :string
    field :parent_id, :integer
    belongs_to :parent, Comment, foreign_key: :parent_id, references: :id, define_field: false
    has_many :children, Comment, foreign_key: :parent_id, references: :id
  end
end

Фильтрация связей

Можно указать параметр :where, который будет фильтровать записи, возвращаемые связью. Запрос, соединение или предварительная загрузка связи будут использовать заданные условия, как показано ниже:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    has_many :public_comments, Comment,
      where: [public: true]
  end
end

Параметр :where принимает список ключевых слов, где ключ — атом, представляющий поле, а значение — одно из следующих:

  • nil — указывает, что поле должно иметь значение nil
  • {:not, nil} — указывает, что поле не должно иметь значение nil
  • {:in, list} — указывает, что поле должно иметь одно из значений списка
  • {:fragment, expr} — задаёт строку фрагмента в качестве фильтра (см. Ecto.Query.API.fragment/1); значение поля передаётся ей как единственный аргумент
  • или любое другое значение, с которым напрямую сравнивается поле

Обратите внимание: перечисленные выше значения отличаются от значений, которые вы передавали бы в where при построении запроса. Например, попытка построить запрос вида

from Post, where: [id: nil]

вызовет ошибку. Это происходит потому, что запросы можно строить динамически, поэтому передача nil может привести к ошибкам безопасности. Однако значения :where для связи задаются во время компиляции, что обеспечивает меньшую динамичность и не позволяет использовать все возможности запросов Ecto. Поэтому у них разные API.

Важно! Используйте эту возможность только в случае крайней необходимости: иначе легко получить большие схемы с десятками разных связей, загромождающих схему и снижающих производительность приложения. Например, если связи нужны вам только для разных вариантов запросов, лучше строить и комбинировать запросы. Так, вместо двух связей — одной для комментариев и другой для удалённых комментариев — можно использовать одну связь с комментариями и фильтровать её:

posts
|> Ecto.assoc(:comments)
|> Comment.deleted()

Или при предварительной загрузке:

from posts, preload: [comments: ^Comment.deleted()]

has_many/has_one :through

Ecto также позволяет определять связи на основе других связей с помощью параметра :through. Рассмотрим пример:

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    has_many :comments, Comment
    has_one :permalink, Permalink

    # In the has_many :through example below, the `:comments`
    # in the list [:comments, :author] refers to the
    # `has_many :comments` in the Post own schema and the
    # `:author` refers to the `belongs_to :author` of the
    # Comment's schema (the module below).
    # (see the description below for more details)
    has_many :comments_authors, through: [:comments, :author]

    # Specify the association with custom source
    has_many :tags, {"posts_tags", Tag}
  end
end

defmodule Comment do
  use Ecto.Schema

  schema "comments" do
    belongs_to :author, Author
    belongs_to :post, Post
    has_one :post_permalink, through: [:post, :permalink]
  end
end

В примере выше мы определили связь has_many :through с именем :comments_authors. Связь :through всегда ожидает список, причём первым элементом списка должна быть ранее определённая связь в текущем модуле. Например, :comments_authors сначала указывает на :comments в том же модуле (Post), а затем — на :author в следующей схеме, Comment.

Эта связь :through вернёт всех авторов всех комментариев, относящихся к этой публикации:

# Get all comments authors for a given post
post = Repo.get(Post, 42)
authors = Repo.all assoc(post, :comments_authors)

Связи :through также можно предварительно загружать. В этом случае предварительно загружается не только связь :through, но и все промежуточные звенья:

[post] = Repo.all(from(p in Post, where: p.id == 42, preload: :comments_authors))
post.comments_authors #=> [%Author{...}, ...]

# The comments for each post will be preloaded too
post.comments #=> [%Comment{...}, ...]

# And the author for each comment too
hd(post.comments).author #=> %Author{...}

Если ожидается, что связь :through вернёт один или ноль элементов, вместо неё следует использовать has_one :through, как в примере в начале этого раздела:

# How we defined the association above in Comments
has_one :post_permalink, through: [:post, :permalink]

# Get a preloaded comment
[comment] = Repo.all(Comment) |> Repo.preload(:post_permalink)
comment.post_permalink #=> %Permalink{...}

Если возможно, Ecto избегает обхода промежуточных связей в запросах. Например, в приведённом выше примере у Comment есть столбец post_id (определённый с помощью belongs_to :post), и ожидается, что у Permalink будет такой же столбец. Поэтому при предварительной загрузке постоянных ссылок Ecto может вовсе не обращаться к таблице "posts". Разумеется, это предполагает, что ваша база данных гарантирует корректность этих ссылок. Этого можно добиться, определив ограничения внешних ключей и ссылки в базе данных (часто это делается с помощью миграций EctoSQL).

Обратите внимание, что связи :through доступны только для чтения. Например, нельзя использовать Ecto.Changeset.cast_assoc/3 для изменения данных через такие связи.

has_one(name, schema, opts \\ [])Источник

Обозначает связь «один к одному» с другой схемой.

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

Параметры

  • :foreign_key - задаёт внешний ключ. Он должен соответствовать полю в другой схеме; по умолчанию используется имя текущего модуля в нижнем регистре с подчёркиваниями, дополненное суффиксом _id

  • :references - задаёт ключ в текущей схеме, который будет использоваться для связи; по умолчанию используется первичный ключ схемы

  • :through - указывает, что эту связь нужно определить на основе существующих связей. Дополнительные сведения см. в разделе has_many/3

  • :on_delete - действие, выполняемое со связями при удалении родительской записи. Возможны значения :nothing (по умолчанию), :nilify_all и :delete_all. Для большинства реляционных баз данных ИСПОЛЬЗОВАТЬ этот параметр НЕ РЕКОМЕНДУЕТСЯ. Вместо этого задайте в миграции references(:parent_id, on_delete: :delete_all). В отличие от параметра миграции, этот параметр не гарантирует целостность данных и срабатывает только при вызове Ecto.Repo.delete/2 (но не при вызове Ecto.Repo.delete_all/2); каскадное удаление не выполняется. Если у публикаций есть комментарии, а у комментариев — теги, при удалении публикации удалятся только комментарии. Если ваша база данных не поддерживает ссылки, каскадное удаление можно реализовать вручную с помощью Ecto.Multi или Ecto.Changeset.prepare_changes/2

  • :on_replace - действие, выполняемое со связями, когда запись заменяется при приведении типов или изменении родительского changeset. Возможны значения :raise (по умолчанию), :mark_as_invalid, :nilify, :update или :delete. Дополнительные сведения о связанных данных см. в документации Ecto.Changeset.

  • :defaults - значения по умолчанию, используемые при создании связи. Это может быть список ключевых слов с параметрами, переопределяющими схему связи, или atom/{module, function, args}, принимающий структуру связи и структуру владельца в качестве аргументов. Например, если задать Post.has_one :banner, defaults: [public: true], то при использовании Ecto.build_assoc(post, :banner) баннер будет содержать banner.public == true. Также можно задать Post.has_one :banner, defaults: :update_banner, чтобы вызвать Post.update_banner(banner, post), или указать кортеж MFA, например {Mod, fun, [arg3, arg4]}, чтобы вызвать Mod.fun(banner, post, arg3, arg4)

  • :where - фильтр для связи. При загрузке связей has_one Ecto формирует запрос с установленным значением LIMIT, равным единице. Если связь может возвращать несколько записей, этот параметр позволит гарантировать, что будет возвращён один уникальный результат. См. раздел «Фильтрация связей» в документации has_many/3. Не применяется к связям :through.

Примеры

defmodule Post do
  use Ecto.Schema

  schema "posts" do
    has_one :permalink, Permalink

    # Specify the association with custom source
    has_one :category, {"posts_categories", Category}
  end
end

# The permalink can come preloaded on the post struct
[post] = Repo.all(from(p in Post, where: p.id == 42, preload: :permalink))
post.permalink #=> %Permalink{...}

При использовании EctoSQL внешний ключ необходимо определить в таблицах permalinks и categories, как показано в примерах belongs_to/3. Дополнительные примеры приведены в справочнике по связям.

many_to_many(name, schema, opts \\ [])Источник

Обозначает связь «многие ко многим» с другой схемой.

Связь реализуется через схему или источник соединения, содержащие внешние ключи для связанных схем. Например, следующая связь:

# from MyApp.Post
many_to_many :tags, MyApp.Tag, join_through: "posts_tags"

поддерживается реляционными базами данных с помощью таблицы соединения, как показано ниже:

[Post] <-> [posts_tags] <-> [Tag]
  id   <--   post_id
              tag_id    -->  id

Дополнительная информация о миграции для создания такой схемы приведена ниже.

Параметры

  • :join_through — задаёт источник связанных данных. Это может быть строка, например "posts_tags", обозначающая базовую таблицу хранилища, или атом, например MyApp.PostTag, обозначающий схему. Этот параметр обязателен.

  • :join_keys — задаёт способ связывания схем. Ожидается список ключевых слов с двумя элементами: первый определяет, как таблица соединения связана с текущей схемой, а второй — как она связана со связанной схемой. В примере выше значение по умолчанию: [post_id: :id, tag_id: :id]. Ключи образуются из имён схем по правилам словообразования.

  • :on_delete — действие, выполняемое со связями при удалении родительской записи. Может быть :nothing (по умолчанию) или :delete_all. Для большинства реляционных баз данных использовать этот параметр НЕ РЕКОМЕНДУЕТСЯ. Вместо этого задайте в миграции references(:parent_id, on_delete: :delete_all). В отличие от параметра миграции, этот параметр не гарантирует целостность данных и срабатывает только при вызове Ecto.Repo.delete/2 (но не при Ecto.Repo.delete_all/2). Этот параметр может удалять данные только из источника соединения, но не связанные записи, и не выполняет каскадное удаление.

  • :on_replace — действие, выполняемое со связями при замене записи во время приведения типов или изменения родительского набора изменений. Может быть :raise (по умолчанию), :mark_as_invalid или :delete. :delete удаляет данные только из источника соединения, но не связанные записи. Дополнительные сведения см. в разделе о связанных данных в Ecto.Changeset.

  • :on_join_through_conflict — если связь входит в операцию вставки, Ecto автоматически попытается создать соответствующую запись в таблице :join_through. Этот параметр позволяет настроить поведение при разрешении конфликта, если запись уже существует. Допустимые значения: :raise или :nothing. Значение по умолчанию — :raise

  • :defaults — значения по умолчанию, используемые при создании связи. Это может быть список ключевых слов с параметрами, переопределяющими схему связи, либо atom/{module, function, args}, принимающий структуру связи и структуру владельца в качестве аргументов. Например, если задать Post.many_to_many :tags, defaults: [public: true], при использовании Ecto.build_assoc(post, :tags) метка получит значение tag.public == true. Также можно задать Post.many_to_many :tags, defaults: :update_tag, чтобы вызвать Post.update_tag(tag, post), или указать кортеж MFA, например {Mod, fun, [arg3, arg4]}, чтобы вызвать Mod.fun(tag, post, arg3, arg4)

  • :join_defaults — то же, что и :defaults, но применяется к схеме соединения. Если задан этот параметр, а значение :join_through не является схемой, будет вызвана ошибка.

  • :unique — если задано значение true, проверяет уникальность связанных записей при приведении связи или её изменении через родительскую запись. Например, проверяется, что одну и ту же метку нельзя прикрепить к одной записи более одного раза. Эта проверка предназначена главным образом для быстрого предоставления обратной связи пользователю и не гарантирует уникальность на уровне базы данных. Поэтому следует также создать уникальный индекс в таблице соединения базы данных, например: create unique_index(:posts_tags, [:post_id, :tag_id])

  • :where — фильтр для связи. См. раздел «Фильтрация связей» в has_many/3

  • :join_where — фильтр для таблицы соединения. См. раздел «Фильтрация связей» в has_many/3

  • :preload_order — задаёт порядок order_by при предварительной загрузке связи. Это может быть список ключевых слов/список полей или кортеж MFA, например {Mod, fun, []}. В обоих случаях должно возвращаться корректное выражение order_by. Дополнительные сведения о выражениях сортировки см. в разделе Ecto.Query.order_by/3. Обратите внимание: если предоставить собственный запрос с собственной секцией order_by, приоритет будет у пользовательской сортировки, а :preload_order применяться не будет. О том, как использовать этот параметр, см. раздел Порядок предварительной загрузки ниже

Использование Ecto.assoc/2

Одно из преимуществ использования many_to_many заключается в том, что Ecto по возможности не загружает промежуточную таблицу, повышая эффективность запросов. Поэтому разработчикам не следует обращаться к таблице соединения many_to_many в запросах. Доступ к таблице соединения возможен лишь в некоторых случаях, например с помощью Ecto.assoc/2. Например, если написать так:

post
|> Ecto.assoc(:tags)
|> where([t, _pt, p], p.public == t.public)

это может работать не так, как ожидалось, поскольку таблица posts_tags может не входить в запрос. Эту проблему можно решить несколькими способами. Один из вариантов — использовать ...:

post
|> Ecto.assoc(:tags)
|> where([t, ..., p], p.public == t.public)

Другой, предпочтительный вариант — переписать запрос с явным join, не указывая промежуточные привязки, поскольку они разрешаются только позже:

# keyword syntax
from t in Tag,
  join: p in assoc(t, :post), on: p.id == ^post.id

# pipe syntax
Tag
|> join(:inner, [t], p in assoc(t, :post), on: p.id == ^post.id)

Если необходимо обращаться к таблице соединения, вероятно, следует использовать has_many/3 с параметром :through.

Удаление данных

При удалении связанных данных many_to_many Ecto всегда удаляет данные из схемы соединения, но никогда — из целевых связей: независимо от того, задан ли для :on_replace параметр :delete, для :on_delete — :delete_all или используются ли функции набора изменений, например Ecto.Changeset.put_assoc/3. Например, если у Post есть связь «многие ко многим» с Tag, то при задании :on_delete значения :delete_all будут удалены только записи из таблицы "posts_tags", если удаляется Post.

Миграция

Структура миграции зависит от значения, переданного в :join_through. Если :join_through — это просто строка, обозначающая таблицу, можно определить таблицу без первичных ключей. Не следует добавлять в неё другие столбцы, поскольку Ecto не будет задавать их значения:

create table(:posts_tags, primary_key: false) do
  add :post_id, references(:posts, on_delete: :delete_all), null: false
  add :tag_id, references(:tags, on_delete: :delete_all), null: false
end

Однако если :join_through — это схема, например MyApp.PostTag, таблица соединения может иметь ту же структуру, что и любая другая таблица в кодовой базе, включая временные метки:

create table(:posts_tags) do
  add :post_id, references(:posts, on_delete: :delete_all), null: false
  add :tag_id, references(:tags, on_delete: :delete_all), null: false
  timestamps()
end

Поскольку :join_through содержит схему, в таких случаях Ecto автоматически обрабатывает автоматически генерируемые значения и первичные ключи.

Порядок предварительной загрузки

Параметр :preload_order можно использовать, чтобы возвращать предварительно загруженные структуры в детерминированном порядке. Он принимает список ключевых слов/список, заданный во время компиляции, либо кортеж MFA, например {Mod, fun, []}. Кортеж MFA используется для формирования выражения order_by во время выполнения.

При указании списка ключевых слов/списка, заданного во время компиляции, сортировка применяется к таблице связи, а не к таблице соединения. Для сортировки по таблице соединения можно задать кортеж MFA, использующий Ecto.Query.dynamic/2.

Предположим, что у нас есть связь Assoc, соединённая через таблицу join_through. Запрос предварительной загрузки, который Ecto генерирует по умолчанию, примерно такой:

from a in Assoc, join: jt in "join_through", on: ...

Если :preload_order задано как [asc: :field], запрос предварительной загрузки будет изменён следующим образом:

from a in Assoc, join: jt in "join_through", on: ..., order_by: [asc: a.field]

Аналогично, все списки ключевых слов/списки, заданные во время компиляции, будут интерпретироваться так, будто их поля принадлежат таблице связи. Чтобы отсортировать по полю из таблицы соединения, можно задать кортеж MFA, использующий Ecto.Query.dynamic/2.

Например, если :preload_order задано как {Mod, fun, []}, что соответствует следующей функции:

defmodule Mod do
  def fun() do
    [desc: dynamic([assoc, join], join.field)]
  end
end

запрос предварительной загрузки будет изменён следующим образом:

from a in Assoc, join: jt in "join_through", on: ..., order_by: [desc: jt.field]

Обратите внимание на порядок привязок. Таблица соединения всегда находится последней.

Примеры

defmodule Post do
  use Ecto.Schema
  schema "posts" do
    many_to_many :tags, Tag, join_through: "posts_tags"
  end
end

# Let's create a post and a tag
post = Repo.insert!(%Post{})
tag = Repo.insert!(%Tag{name: "introduction"})

# We can associate at any time post and tags together using changesets
post
|> Repo.preload(:tags) # Load existing data
|> Ecto.Changeset.change() # Build the changeset
|> Ecto.Changeset.put_assoc(:tags, [tag]) # Set the association
|> Repo.update!

# In a later moment, we may get all tags for a given post
post = Repo.get(Post, 42)
tags = Repo.all(assoc(post, :tags))

# The tags may also be preloaded on the post struct for reading
[post] = Repo.all(from(p in Post, where: p.id == 42, preload: :tags))
post.tags #=> [%Tag{...}, ...]

Пример схемы соединения

Для обработки связей many_to_many можно использовать схему соединения. Благодаря независимому характеру Ecto можно создать структуру «соединения», которая belongs_to обе стороны связи «многие ко многим».

В нашем примере у User есть связь «многие ко многим» с Organizations:

defmodule MyApp.Repo.Migrations.CreateUserOrganization do
  use Ecto.Migration

  def change do
    create table(:users_organizations) do
      add :user_id, references(:users)
      add :organization_id, references(:organizations)

      timestamps()
    end
  end
end

defmodule UserOrganization do
  use Ecto.Schema

  @primary_key false
  schema "users_organizations" do
    belongs_to :user, User
    belongs_to :organization, Organization
    timestamps() # Added bonus, a join schema will also allow you to set timestamps
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> Ecto.Changeset.cast(params, [:user_id, :organization_id])
    |> Ecto.Changeset.validate_required([:user_id, :organization_id])
    # Maybe do some counter caching here!
  end
end

defmodule User do
  use Ecto.Schema

  schema "users" do
    many_to_many :organizations, Organization, join_through: UserOrganization
  end
end

defmodule Organization do
  use Ecto.Schema

  schema "organizations" do
    many_to_many :users, User, join_through: UserOrganization
  end
end

Чтобы создать связь, передайте идентификаторы существующих User и Organization в UserOrganization.changeset/2:

changeset = UserOrganization.changeset(%UserOrganization{}, %{user_id: id, organization_id: id})

case Repo.insert(changeset) do
  {:ok, assoc} -> # Assoc was created!
  {:error, changeset} -> # Handle the error
end

schema(source, list)Источник

Определяет структуру схемы с именем источника и определениями полей.

В структуру добавляется дополнительное поле __meta__ для хранения внутреннего состояния Ecto. Значением этого поля всегда является структура Ecto.Schema.Metadata; изменить его можно с помощью функции Ecto.put_meta/2.

timestamps(opts \\ [])Источник

Создаёт поля временных меток :inserted_at и :updated_at.

Значения полей, созданных этой макрокомандой, автоматически устанавливаются на текущее время при вставке и обновлении записей в репозитории.

Параметры

  • :inserted_at — имя поля схемы Ecto для времени вставки или false
  • :updated_at — имя поля схемы Ecto для времени обновления или false
  • :inserted_at_source — имя столбца базы данных для времени вставки или false
  • :updated_at_source — имя столбца базы данных для времени обновления или false
  • :type — тип временных меток; по умолчанию :naive_datetime.
  • :autogenerate — кортеж «модуль-функция-аргументы», используемый для создания временных меток inserted_at и updated_at

Все параметры можно предварительно настроить, задав @timestamps_opts.

Пакет Hex Предварительный просмотр Hex Просмотреть llms.txt Скачать версию ePub

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

© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/Ecto.Schema.html

Spec-Zone.ru

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