View Source Associations
In this document, "Internal data" represents data or logic hardcoded into your Elixir code. "External data" means data that comes from the user via forms, APIs, and often need to be normalized, pruned, and validated via Ecto.Changeset
.
Has many / belongs to
The has many association
defmodule Movie do
use Ecto.Schema
schema "movies" do
field :title, :string
field :release_date, :date
has_many :characters, Character
end
end
The belongs to association
defmodule Character do
use Ecto.Schema
schema "characters" do
field :name, :string
field :age, :integer
belongs_to :movie, Movie
end
end
Has one / belongs to
The has one association
defmodule Movie do
use Ecto.Schema
schema "movies" do
field :title, :string
field :release_date, :date
has_one :screenplay, Screenplay
end
end
The belongs association
defmodule Screenplay do
use Ecto.Schema
schema "screenplays" do
field :lead_writer, :string
belongs_to :movie, Movie
end
end
Many to many
Through a join table
The first schema
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
The second schema
defmodule Actor do
use Ecto.Schema
schema "actors" do
field :name, :string
many_to_many :movies, Movie, join_through: "movies_actors"
end
end
Through a join schema
The first schema
defmodule User do
use Ecto.Schema
schema "users" do
many_to_many :organizations, Organization, join_through: UserOrganization
end
end
The second schema
defmodule Organization do
use Ecto.Schema
schema "organizations" do
many_to_many :users, User, join_through: UserOrganization
end
end
The join schema
defmodule UserOrganization do
use Ecto.Schema
@primary_key false
schema "users_organizations" do
belongs_to :user, User
belongs_to :organization, Organization
timestamps()
end
end
Querying associated records
Preloading in the parent record query
query = from m in Movie, preload: :characters
Repo.all(query)
Preloading when parent records are already loaded
movies = Repo.all(Movie)
movies = Repo.preload(movies, :characters)
Preloading with join to generate a single query
Regular join
query =
from m in Movie,
join: c in Character,
on: m.id == c.movie_id,
preload: [characters: c]
Repo.all(query)
Join using assoc
query =
from m in Movie,
join: c in assoc(m, :characters),
preload: [characters: c]
Repo.all(query)
Inserting associated records
Inserting a child record to an existing parent
Using internal data
Repo.get_by!(Movie, title: "The Shawshank Redemption")
|> Ecto.build_assoc(:characters, name: "Red", age: 60)
|> Repo.insert()
Using external data
# 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()
Inserting parent and child records together
Using internal data
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}
]
}
)
Using external data
# 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()
Updating associated records
Updating records individually
For individual updates, fetch and update records directly
movie =
Repo.get_by!(Movie, title: "The Shawshank Redemption")
|> Repo.preload(:screenplay)
movie.screenplay
|> change(%{lead_writer: "Frank Darabont"})
|> Repo.update()
Updating all associated records, using internal data
Using 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]
Note: the example above performs the same operation on all entries, therefore it can be written as a query. Queries should be preferred when possible as they avoid loading all data into memory and are more performant. See next example.
Using 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])
Updating all associated records, using external data
Using 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}]
When using Ecto.Changeset.cast_assoc/3
:
- Entries without ID are added.
- Existing entries with matching IDs are updated.
- Existing entries without matching IDs will raise
but it can be configured using
:on_replace
. - Additional options are supported to customize casting, sorting, and deletion