Ассоциации
ИсточникВ этом документе «внутренние данные» — это данные или логика, жёстко заданные в вашем коде Elixir. «Внешние данные» — это данные, поступающие от пользователя через формы и API; их часто необходимо нормализовать, отфильтровать и проверить с помощью Ecto.Changeset. Здесь также приведены примеры миграций в соответствии с EctoSQL.
Один ко многим / принадлежит
Ассоциация «один ко многим»
defmodule Movie do
use Ecto.Schema
schema "movies" do
field :title, :string
field :release_date, :date
has_many :characters, Character
end
endАссоциация «принадлежит»
defmodule Character do
use Ecto.Schema
schema "characters" do
field :name, :string
field :age, :integer
belongs_to :movie, Movie
end
endМиграция
defmodule MyApp.Migrations.CreateMoviesAndCharacters do
use Ecto.Migration
def change do
create table("movies") do
add :title, :string, null: false
add :release_date, :date
timestamps()
end
# The foreign key is in the belongs_to schema
create table("characters") do
add :name, :string, null: false
add :age, :integer
add :movie_id,
references(:movies, on_delete: :delete_all),
null: false
timestamps()
end
end
endОдин к одному / принадлежит
Ассоциация «один к одному»
defmodule Movie do
use Ecto.Schema
schema "movies" do
field :title, :string
field :release_date, :date
has_one :screenplay, Screenplay
end
endАссоциация «принадлежит»
defmodule Screenplay do
use Ecto.Schema
schema "screenplays" do
field :lead_writer, :string
belongs_to :movie, Movie
end
endМиграция
defmodule MyApp.Migrations.CreateMoviesAndPlays do
use Ecto.Migration
def change do
create table("movies") do
add :title, :string, null: false
add :release_date, :date
timestamps()
end
# The foreign key is in the belongs_to schema
create table("screenplays") do
add :lead_writer, :string, null: false
add :movie_id,
references(:movies, on_delete: :delete_all),
null: false
timestamps()
end
end
endМногие ко многим
Через таблицу соединения
Первая схема
defmodule Movie do
use Ecto.Schema
schema "movies" do
field :title, :string
field :release_date, :date
many_to_many :actors, Actor, join_through: "movies_actors"
end
end
Вторая схема
defmodule Actor do
use Ecto.Schema
schema "actors" do
field :name, :string
many_to_many :movies, Movie, join_through: "movies_actors"
end
endЧерез схему соединения
Первая схема
defmodule Movie do
use Ecto.Schema
schema "movies" do
many_to_many :actors, Actor, join_through: MovieActor
end
end
Вторая схема
defmodule Actor do
use Ecto.Schema
schema "actors" do
many_to_many :movies, Movie, join_through: MovieActor
end
end
Схема соединения
defmodule MovieActor do
use Ecto.Schema
@primary_key false
schema "movies_actors" do
belongs_to :movie, Movie
belongs_to :actor, Actor
timestamps()
end
endМиграция
defmodule MyApp.Migrations.CreateMoviesAndActors do
use Ecto.Migration
def change do
create table("movies") do
timestamps()
end
create table("actors") do
timestamps()
end
end
end
Без схемы соединения таблица соединения может содержать только внешние ключи
defmodule MyApp.Migrations.CreateJoinTable do
use Ecto.Migration
def change do
create table("movies_actors", primary_key: false) do
add :movie_id,
references(:movies, on_delete: :delete_all),
null: false
add :actor_id,
references(:actors, on_delete: :delete_all),
null: false
end
create unique_index(:movies_actors, [:movie_id, :actor_id])
end
end
Со схемой соединения таблица соединения может содержать другие столбцы, например временные метки
defmodule MyApp.Migrations.CreateJoinTable do
use Ecto.Migration
def change do
create table("movies_actors", primary_key: false) do
add :movie_id,
references(:movies, on_delete: :delete_all),
null: false
add :actor_id,
references(:actors, on_delete: :delete_all),
null: false
timestamps()
end
create unique_index(:movies_actors, [:movie_id, :actor_id])
end
endЗапрос связанных записей
Предварительная загрузка в запросе родительской записи
query = from m in Movie, preload: :characters Repo.all(query)
Предварительная загрузка, когда родительские записи уже загружены
movies = Repo.all(Movie) movies = Repo.preload(movies, :characters)
Предварительная загрузка с помощью join для формирования одного запроса
Обычный join
query = from m in Movie, join: c in Character, on: m.id == c.movie_id, preload: [characters: c] Repo.all(query)
Join с использованием assoc
query = from m in Movie, join: c in assoc(m, :characters), preload: [characters: c] Repo.all(query)
Вставка связанных записей
Вставка дочерней записи для существующей родительской записи
Использование внутренних данных
Repo.get_by!(Movie, title: "The Shawshank Redemption") |> Ecto.build_assoc(:characters, name: "Red", age: 60) |> Repo.insert()
Использование внешних данных
# Params represent data from a form, API, CLI, etc
params = %{"name" => "Red", "age" => 60}
Repo.get_by!(Movie, title: "The Shawshank Redemption")
|> Ecto.build_assoc(:characters)
|> cast(params, [:name, :age])
|> Repo.insert()Совместная вставка родительской и дочерней записей
Использование внутренних данных
Repo.insert(
%Movie{
title: "The Shawshank Redemption",
release_date: ~D[1994-10-14],
characters: [
%Character{name: "Andy Dufresne", age: 50},
%Character{name: "Red", age: 60}
]
}
)
Использование внешних данных
# Params represent data from a form, API, CLI, etc
params = %{
"title" => "Shawshank Redemption",
"release_date" => "1994-10-14",
"characters" =>
[
%{"name" => "Andy Dufresne", "age" => "50"},
%{"name" => "Red", "age" => "60"}
]
}
%Movie{}
|> cast(params, [:title, :release_date])
|> cast_assoc(:characters)
|> Repo.insert()Обновление связанных записей
Обновление записей по отдельности
Для обновления отдельных записей получайте и обновляйте их напрямую
movie =
Repo.get_by!(Movie, title: "The Shawshank Redemption")
|> Repo.preload(:screenplay)
movie.screenplay
|> change(%{lead_writer: "Frank Darabont"})
|> Repo.update()Обновление всех связанных записей с использованием внутренних данных
Использование Ecto.Changeset.put_assoc/3
movie =
Repo.get_by!(Movie, title: "The Shawshank Redemption")
|> Repo.preload(:characters)
IO.inspect(movie.characters)
#=> [%Character{name: "Andy Dufresne", age: 50},
#=> %Character{name: "Red", age: 60}]
characters =
Enum.map(movie.characters, fn character ->
change(character, age: character.age + 1)
end)
{:ok, movie} =
movie
|> change()
|> put_assoc(:characters, characters)
|> Repo.update()
movie.characters |> Enum.map(&(&1.age)) |> IO.inspect
#=> [51, 61]
Примечание: приведённый выше пример выполняет одну и ту же операцию для всех записей, поэтому его можно записать в виде запроса. По возможности предпочтительнее использовать запросы: они позволяют не загружать все данные в память и работают эффективнее. См. следующий пример.
Использование Ecto.Repo.update_all/3
movie = Repo.get_by!(Movie, title: "The Shawshank Redemption") movie # Query to load all characters associated to a given movie |> Ecto.assoc(:characters) |> Repo.update_all(inc: [age: 1])
Обновление всех связанных записей с использованием внешних данных
Использование Ecto.Changeset.cast_assoc/3
# Params represent data from a form, API, CLI, etc
params = %{
"director" => "Frank Darabont",
"characters" => [
%{"id" => 1, "name" => "Andy Dufresne"},
%{"name" => "Red", "age" => 60}
]
}
movie =
Repo.get_by!(Movie, title: "The Shawshank Redemption")
|> Repo.preload(:characters)
IO.inspect(movie.characters)
#=> [%{id: 1, name: "Andy", age: 50}]
{:ok, movie} =
movie
|> cast(params, ["director"])
|> cast_assoc(:characters)
|> Repo.update()
IO.inspect(movie.characters)
#=> [%{id: 1, name: "Andy Dufresne", age: 50},
#=> %{id: 2, name: "Red", age: 60}]
При использовании Ecto.Changeset.cast_assoc/3:
- Записи без идентификатора добавляются.
- Существующие записи с совпадающими идентификаторами обновляются.
- Для существующих записей без совпадающих идентификаторов возникает ошибка, но это поведение можно настроить с помощью
:on_replace. - Поддерживаются дополнительные параметры для настройки приведения типов, сортировки и удаления.
© 2014 Chris McCord
Licensed under the MIT License.
https://ecto.hexdocs.pm/associations.html