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

Has many / belongs to

the-has-many-association

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

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

Has one / belongs to

the-has-one-association

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

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

Many to many

through-a-join-table

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

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

Querying associated records

preloading-in-the-parent-record-query

Preloading in the parent record query

query = from m in Movie, preload: :characters
Repo.all(query)

preloading-when-parent-records-are-already-loaded

Preloading when parent records are already loaded

movies = Repo.all(Movie)
movies = Repo.preload(movies, :characters)

preloading-with-join-to-generate-a-single-query

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 associated records

inserting-a-child-record-to-an-existing-parent

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

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 associated records

updating-records-individually

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

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

Updating all associated records, using external data

Using Ecto.Changeset.cast_assoc/3

# Params represent data from a form, API, CLI, etc
params = [
  %{"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
  |> change()
  |> cast_assoc(:characters, params)
  |> Repo.update

IO.inspect(movie.characters)
#=> [%{id: 1, name: "Andy", 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.