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
Repo.all(query)

Join using assoc

query =
  from m in Movie,
  join: c in assoc(m, :characters),
  preload: :characters
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_writter: "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)
#=> [%{name: "Andy Dufresne", age: 50},
#=>  %{name: "Red", age: 60}]

characters =
  Enum.map(characters, fn character ->
    update_in(character.age, &(&1 + 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