アソシエーション
このセクションでは、スキーマ間のアソシエーションを定義し、扱うためにEctoを使用する方法について学びます。
セットアップ
前回のレッスンの Friends
アプリから始めましょう。 ここ のセットアップを見るとすぐに思い出せます。
アソシエーションの種類
スキーマ間で定義することができるアソシーションは3つあります。それらがどういうものか、そして各種類の関係をどのように実装するのかを見ていきます。
従属/1対多
私たちのお気に入りの映画をカタログを作れるように、デモアプリのドメインモデルにいくつかの新しいエンティティを追加します。まずは Movie
と Character
という2つのスキーマから始めます。これらのスキーマの間に “1対多/従属” の関係を実装して、映画(movie)は複数のキャラクター(character)を持ち、キャラクターは映画に従属するようにします。
1対多マイグレーション
Movie
のマイグレーションを作ってみましょう:
mix ecto.gen.migration create_movies
新しく作られたマイグレーションファイルを開き、いくつか属性を持った movies
テーブルを作るために change
関数を定義しましょう:
# priv/repo/migrations/*_create_movies.exs
defmodule Friends.Repo.Migrations.CreateMovies do
use Ecto.Migration
def change do
create table(:movies) do
add :title, :string
add :tagline, :string
end
end
end
1対多スキーマ
映画とキャラクターとの間に”1対多”の関係を指定するスキーマを追加します。
# lib/example/movie.ex
defmodule Friends.Movie do
use Ecto.Schema
schema "movies" do
field :title, :string
field :tagline, :string
has_many :characters, Friends.Character
end
end
has_many/3
マクロはデータベースそのものには何も追加しません。これは関連付けられたスキーマである characters
の外部キーを使用し、映画に関連するキャラクターを利用可能にします。これによって movie.characters
が使用可能となります。
従属マイグレーション
これで Character
マイグレーションとスキーマを構築する準備ができました。キャラクターは映画に従属するので、この関係を示すマイグレーションとスキーマを定義します。
まずは、マイグレーションを生成します:
mix ecto.gen.migration create_characters
映画に従属するキャラクターを定義するためには、 movie_id
を持つ characters
テーブルが必要です。このカラムは外部キーとして機能させたいです。これは、 create_table/1
関数に次の行を追加することで実現できます:
add :movie_id, references(:movies)
つまりマイグレーションファイルはこのようになります:
# priv/migrations/*_create_characters.exs
defmodule Friends.Repo.Migrations.CreateCharacters do
use Ecto.Migration
def change do
create table(:characters) do
add :name, :string
add :movie_id, references(:movies)
end
end
end
従属 スキーマ
スキーマもまたキャラクターと映画の間に”従属”の関係を定義する必要があります。
# lib/example/character.ex
defmodule Friends.Character do
use Ecto.Schema
schema "characters" do
field :name, :string
belongs_to :movie, Friends.Movie
end
end
belongs_to/3
マクロが何をするのか詳しく見てみましょう。 スキーマへの外部キーの movie_id
の追加に加えて、これは characters
を通して関連した movies
スキーマにアクセスする機能を 提供します 。これは外部キーを使用してキャラクターのクエリを実行する際に、キャラクターに関連した映画を参照できるようにします。これによって character.movie
を使えるようになります。
これでマイグレーションを実行する準備ができました:
mix ecto.migrate
従属/1対1
映画は1つの配信者(distributor)を持っているとしましょう。例えばNetflixは彼らのオリジナル映画 “Bright” の配信者です。
“従属”の関係を使って Distributor
のマイグレーションとスキーマを定義します。まずは、マイグレーションを生成しましょう:
mix ecto.gen.migration create_distributors
生成した distributors
テーブルのマイグレーションに、外部キーの movie_id
と、映画の配信者が1人であることを示すユニークインデックスを追加する必要があります。
# priv/repo/migrations/*_create_distributors.exs
defmodule Friends.Repo.Migrations.CreateDistributors do
use Ecto.Migration
def change do
create table(:distributors) do
add :name, :string
add :movie_id, references(:movies)
end
create unique_index(:distributors, [:movie_id])
end
end
そして Distributor
スキーマは belongs_to/3
マクロを使うことで、 distributor.movie
を使用可能にし、また外部キーによって配信者に関連した映画を見つけられるようにします。
# lib/example/distributor.ex
defmodule Friends.Distributor do
use Ecto.Schema
schema "distributors" do
field :name, :string
belongs_to :movie, Friends.Movie
end
end
次に、”1対1”の関係を Movie
スキーマに追加します:
# lib/example/movie.ex
defmodule Friends.Movie do
use Ecto.Schema
schema "movies" do
field :title, :string
field :tagline, :string
has_many :characters, Friends.Character
has_one :distributor, Friends.Distributor # I'm new!
end
end
has_one/3
マクロは has_many/3
マクロのように機能します。これは、映画の配信者を探してアクセスできるようにするために関連したスキーマの外部キーを 使用します 。これによって movie.distributor
が使えるようになります。
マイグレーション実行の準備ができました:
mix ecto.migrate
多対多
映画は多くの俳優(actor)を持っていて、俳優は1つ以上の映画に従属することができるとしましょう。この関係を実装するために、映画 と 俳優 の 両方 を参照する中間テーブルを構築します。
はじめに、 Actors
マイグレーションを生成しましょう:
mix ecto.gen.migration create_actors
マイグレーションを定義します:
# priv/migrations/*_create_actors.ex
defmodule Friends.Repo.Migrations.CreateActors do
use Ecto.Migration
def change do
create table(:actors) do
add :name, :string
end
end
end
中間テーブルのマイグレーションを生成しましょう:
mix ecto.gen.migration create_movies_actors
2つの外部キーを持つテーブルをマイグレーションで定義します。また、俳優と映画の組み合わせが一意となるようにユニークインデックスを追加します:
# priv/migrations/*_create_movies_actors.ex
defmodule Friends.Repo.Migrations.CreateMoviesActors do
use Ecto.Migration
def change do
create table(:movies_actors) do
add :movie_id, references(:movies)
add :actor_id, references(:actors)
end
create unique_index(:movies_actors, [:movie_id, :actor_id])
end
end
次に、 many_to_many
マクロを Movie
スキーマに追加しましょう:
# lib/example/movie.ex
defmodule Friends.Movie do
use Ecto.Schema
schema "movies" do
field :title, :string
field :tagline, :string
has_many :characters, Friends.Character
has_one :distributor, Friends.Distributor
many_to_many :actors, Friends.Actor, join_through: "movies_actors" # I'm new!
end
end
最後に、 同じ many_to_many
マクロで Actor
スキーマを定義します。
# lib/example/actor.ex
defmodule Friends.Actor do
use Ecto.Schema
schema "actors" do
field :name, :string
many_to_many :movies, Friends.Movie, join_through: "movies_actors"
end
end
マイグレーション実行の準備ができました:
mix ecto.migrate
関連データの保存
レコードを関連するデータと一緒に保存する方法は、レコード間の関係性によって異なります。”従属/1対多”という関係から始めましょう。
従属
Ecto.build_assoc/3 による保存
“従属”の関係では、 build_assoc/3
関数を利用できます。
build_assoc/3
は3つの引数をとります:
- 保存したいレコードの構造体
- アソシエーションの名前
- 保存する関連レコードにアサインしたい属性
映画と関連するキャラクターを保存してみましょう。はじめに、映画のレコードを作ります:
iex> alias Friends.{Movie, Character, Repo}
iex> movie = %Movie{title: "Ready Player One", tagline: "Something about video games"}
%Friends.Movie{
__meta__: %Ecto.Schema.Metadata<:built, "movies">,
actors: %Ecto.Association.NotLoaded<association :actors is not loaded>,
characters: %Ecto.Association.NotLoaded<association :characters is not loaded>,
distributor: %Ecto.Association.NotLoaded<association :distributor is not loaded>,
id: nil,
tagline: "Something about video games",
title: "Ready Player One"
}
iex> movie = Repo.insert!(movie)
次に、関連するキャラクターを作ってデータベースに挿入します:
character = Ecto.build_assoc(movie, :characters, %{name: "Wade Watts"})
%Friends.Character{
__meta__: %Ecto.Schema.Metadata<:built, "characters">,
id: nil,
movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
movie_id: 1,
name: "Wade Watts"
}
Repo.insert!(character)
%Friends.Character{
__meta__: %Ecto.Schema.Metadata<:loaded, "characters">,
id: 1,
movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
movie_id: 1,
name: "Wade Watts"
}
Movie
スキーマの has_many/3
マクロは 映画が複数の :characters
を持つことを示すので、 build_assoc/3
の2つ目の引数として渡したアソシエーションの名前は :characters
そのものだと気がつくでしょう。私たちが作ったキャラクターは、関連する映画のIDが正しくセットされた movie_id
を持っていることがわかります。
build_assoc/3
を使って映画の関連した配信者を保存するために、 build_assoc/3
の2つ目の引数として映画に関連する配信者の_name_を渡すという同様のアプローチを取ります。
iex> distributor = Ecto.build_assoc(movie, :distributor, %{name: "Netflix"})
%Friends.Distributor{
__meta__: %Ecto.Schema.Metadata<:built, "distributors">,
id: nil,
movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
movie_id: 1,
name: "Netflix"
}
iex> Repo.insert!(distributor)
%Friends.Distributor{
__meta__: %Ecto.Schema.Metadata<:loaded, "distributors">,
id: 1,
movie: %Ecto.Association.NotLoaded<association :movie is not loaded>,
movie_id: 1,
name: "Netflix"
}
多対多
Ecto.Changeset.put_assoc/4 による保存
build_assoc/3
のアプローチは多対多の関係では使えません。映画テーブルも俳優テーブルも外部キーを持たないためです。代わりに、Ectoのチェンジセットと put_assoc/4
関数を利用する必要があります。
上で作った映画のレコードを既に持っているとして、俳優レコードを作ってみましょう:
iex> alias Friends.Actor
iex> actor = %Actor{name: "Tyler Sheridan"}
%Friends.Actor{
__meta__: %Ecto.Schema.Metadata<:built, "actors">,
id: nil,
movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
name: "Tyler Sheridan"
}
iex> actor = Repo.insert!(actor)
%Friends.Actor{
__meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
id: 1,
movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
name: "Tyler Sheridan"
}
これで中間テーブルを通して映画と俳優を関連付ける準備ができました。
まず、チェンジセットを扱うためには、 movie
の構造体に関連するデータを確実に事前ロードしている必要があります。データの事前ロードについてはの後に話します。今のところは、次のようにアソシエーションを事前ロードできるということだけ理解していれば十分です:
iex> movie = Repo.preload(movie, [:distributor, :characters, :actors])
%Friends.Movie{
__meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
actors: [],
characters: [],
distributor: nil,
id: 1,
tagline: "Something about video games",
title: "Ready Player One"
}
次に、映画レコードのためにチェンジセットを作ります:
iex> movie_changeset = Ecto.Changeset.change(movie)
%Ecto.Changeset<action: nil, changes: %{}, errors: [], data: %Friends.Movie<>,
valid?: true>
そして Ecto.Changeset.put_assoc/4
の第1引数としてチェンジセットを渡します:
iex> movie_actors_changeset = movie_changeset |> Ecto.Changeset.put_assoc(:actors, [actor])
%Ecto.Changeset<
action: nil,
changes: %{
actors: [
%Ecto.Changeset<action: :update, changes: %{}, errors: [],
data: %Friends.Actor<>, valid?: true>
]
},
errors: [],
data: %Friends.Movie<>,
valid?: true
>
これにより、次の変更を表す 新しい チェンジセットが作られます: 俳優リストの俳優を映画レコードに追加する。
最後に、最新のチェンジセットを使用して映画と俳優のレコードを更新します:
iex> Repo.update!(movie_actors_changeset)
%Friends.Movie{
__meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
actors: [
%Friends.Actor{
__meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
id: 1,
movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
name: "Bob"
}
],
characters: [],
distributor: nil,
id: 1,
tagline: "Something about video games",
title: "Ready Player One"
}
これによって映画レコードが俳優と適切に関連付けられて、 movie.actors
に事前ロードされていることがわかります。
同じアプローチを使って、映画に関連する新しい俳優を追加できます。 保存された 俳優の構造体を put_assoc/4
に渡す代わりに、単純に作成したい俳優を表す構造体を渡します:
iex> changeset = movie_changeset |> Ecto.Changeset.put_assoc(:actors, [%{name: "Gary"}])
%Ecto.Changeset<
action: nil,
changes: %{
actors: [
%Ecto.Changeset<
action: :insert,
changes: %{name: "Gary"},
errors: [],
data: %Friends.Actor<>,
valid?: true
>
]
},
errors: [],
data: %Friends.Movie<>,
valid?: true
>
iex> Repo.update!(changeset)
%Friends.Movie{
__meta__: %Ecto.Schema.Metadata<:loaded, "movies">,
actors: [
%Friends.Actor{
__meta__: %Ecto.Schema.Metadata<:loaded, "actors">,
id: 2,
movies: %Ecto.Association.NotLoaded<association :movies is not loaded>,
name: "Gary"
}
],
characters: [],
distributor: nil,
id: 1,
tagline: "Something about video games",
title: "Ready Player One"
}
“2”というIDと割り当てた値を持った俳優が作られたことを確認できました。
次のセクションでは、関連付けたレコードにクエリを実行する方法を学びます。
間違いを報告したい、あるいはこのレッスンに貢献したい? このレッスンをGitHubで編集しよう!