Spec-Zone.ru › Phoenix

Ассоциации

Источник

В этом документе «внутренние данные» — это данные или логика, жёстко заданные в вашем коде 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.
  • Поддерживаются дополнительные параметры для настройки приведения типов, сортировки и удаления.
← Предыдущая страница Основные операции CRUD
Следующая страница → Агрегаты и подзапросы

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

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

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

Spec-Zone.ru

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