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.Schemaschema/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(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_oneEcto формирует запрос с установленным значением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.
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/Ecto.Schema.html