關聯關係
在本章節中,將學習如何使用 Ecto 來定義和處理結構描述(schema)之間的關聯關係。
設定
將從上一課的 Friends
應用程式開始。 可以參考 這裡 的設定來快速復習。
關聯關係的種類
可以在結構描述之間定義三種類型的關聯關係。接著將看看它們是什麼以及如何實現每種類型的關係。
屬於/一對多 (Belongs To/Has Many)
將在範例應用程式的域(domain)模型中加入一些新實體,以便可以為喜愛的電影編制目錄。會從兩個結構描述開始:Movie
和 Character
。將實現這兩種結構描述之間的 “屬於/一對多” 關係:一部電影有很多角色,而一個角色屬於一部電影。
一對多 Migration
現在替 Movie
產生成一個 migration:
mix ecto.gen.migration create_movies
打開新產生的 migration 檔案並定義 change
函數以建立帶有一些屬性的 movies
表格:
# 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
一對多 Schema
現在將加入一個結構描述,指定電影及其角色之間的 “一對多” 關係。
# 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
。
屬於 Migration
現在準備建立 Character
的 migration 和結構描述。一個角色屬於一部電影,因此將定義具體指定此關係的 migration 和結構描述。
首先,產生 migration:
mix ecto.gen.migration create_characters
要聲明一個角色屬於一個電影,需要 characters
表格中有一個 movie_id
欄。我們希望此欄函數用作外鍵。可以使用以下這行再 create table/1
函數中完成此操作:
add :movie_id, references(:movies)
所以我們的 migration 應該如下所示:
# 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
屬於 Schema
我們的結構描述同樣需要定義角色與其電影之間的 “屬於” 關係。
# 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
。
現在準備好執行 migrations:
mix ecto.migrate
屬於/一對一 (Belongs To/Has One)
比方說一部電影有一個發行商,例如 Netflix 是原創電影 “Bright” 的發行商。
現在將使用 “屬於” 關係定義 Distributor
migration 和結構描述。首先,來產生 migration:
mix ecto.gen.migration create_distributors
現在應該將 movie_id
的外鍵加入到剛產生的 distributors
表格 migration 中,並加入唯一索引以強制一部電影只有一個發行商:
# 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
接下來,將為 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
巨集一樣。它使用關聯的結構描述外鍵來查找並公開(expose)電影的發行商。這將允許呼用 movie.distributor
。
現在已準備好執行 migrations:
mix ecto.migrate
多對多 (Many To Many)
假定一部電影有很多演員,一個演員可以屬於不止一部電影。現在將構建一個參考 movies 和 actors 這 兩者 的連接(join)表格來實現這種關係。
首先,現在產生 Actors
migration:
mix ecto.gen.migration create_actors
接著定義這個 migration:
# 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
現在產生所需的連接表格 migration:
mix ecto.gen.migration create_movies_actors
現在將定義 migration,使表格具有兩個外鍵。接著還將加入一個唯一的索引來強制 actors 和 movies 的唯一配對:
# 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
現在已經準備好執行 migrations:
mix ecto.migrate
儲存關聯資料
儲存記錄及其關聯資料的方式取決於記錄之間關係的性質。現在從 “屬於/一對多” 的關係開始吧。
屬於 (Belongs To)
經由 Ecto.build_assoc/3 儲存
對於 “屬於” 關係,可以利用 Ecto 的 build_assoc/3
函數。
build_assoc/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
的關聯名稱就是: :characters
。可以看到已經建立了一個角色,其 movie_id
被正確設定為關聯電影的 ID。
為了使用 build_assoc/3
來儲存電影的關聯發行商,我們採用相同的方法將電影和發行商關係的 名稱 作為第二個參數傳遞給 build_assoc/3
:
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"
}
多對多 (Many to Many)
經由 Ecto.Changeset.put_assoc/4 儲存
build_assoc/3
方法對多對多關係不起作用。這是因為 movie 和 actor 表格都沒有包含外鍵。作為替代,需要利用 Ecto 變更集 和 put_assoc/4
函數。
假設已經有了上面建立的 movie 記錄,那麼現在來建立一個 actor 記錄:
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
:
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"
}
可以看到,這給了我們一個有新的 actor 正確關聯的電影記錄,並已經在 movie.actors
中預載。
可以使用相同的方法來建立與指定電影相關聯的全新演員。不必將 已儲存 的 actor 結構體傳遞給put_assoc/4
,只要傳入一個描述想建立新 actor 的 actor 結構體:
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"
}
可以看到建立了一個 ID 為 “2” 的新 actor 和所賦予它的屬性。
在下一章節中,將學習如何查詢關聯記錄。
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!