Skip to main content

guides/nested_associations.md

# Nested Associations

The main reason to reach for Bulkinup over a plain `insert_all/3` is that it writes a parent
and its children at the same time, from a single list of attrs. The parent and each
association are written into their own tables, all within one transaction (by default).

## A `has_many` example

Extending the `Person` example from [Getting Started](getting_started.html) with a
`has_many :pets` association:

`priv/repo/migrations/0002_create_pets.exs`

```elixir
defmodule YourProject.Repo.Migrations.CreatePets do
  use Ecto.Migration

  def change do
    create table(:pets) do
      add :person_id, references(:persons)
      add :name, :string
    end
  end
end
```

`lib/your_project/persons/pet.ex`

```elixir
defmodule YourProject.Persons.Pet do
  use Ecto.Schema
  import Ecto.Changeset

  schema "pets" do
    field :person_id, :integer
    field :name, :string
  end

  def changeset(pet \\ %__MODULE__{}, attrs) do
    pet
    |> cast(attrs, [:id, :person_id, :name])
    |> validate_required([:id, :person_id, :name])
  end
end
```

`lib/your_project/persons/person.ex`

```elixir
defmodule YourProject.Persons.Person do
  use Ecto.Schema
  import Ecto.Changeset

  schema "persons" do
    field :name, :string

    has_many :pets, YourProject.Persons.Pet
  end

  def changeset(person \\ %__MODULE__{}, attrs) do
    person
    |> cast(attrs, [:id, :name])
    |> validate_required([:id, :name])
    |> cast_assoc(:pets)
  end
end
```

> #### Foreign keys are not inferred {: .warning}
>
> Each child's foreign key (here, `person_id`) must be present in its own attrs. Associations
> are written via `insert_all/3`, so the foreign key is not inferred from the parent.

Now a single call writes both the persons and their pets across both tables:

```text
iex> Bulkinup.upsert(
...>   YourProject.Repo,
...>   YourProject.Persons.Person,
...>   [
...>     %{id: 1, name: "Alice", pets: [
...>       %{id: 10, person_id: 1, name: "Rex"},
...>       %{id: 11, person_id: 1, name: "Whiskers"}
...>     ]},
...>     %{id: 2, name: "Bob", pets: [
...>       %{id: 20, person_id: 2, name: "Buddy"}
...>     ]}
...>   ]
...> )
{:ok, %{upserted: 2, skipped: 0}}
```

Running the same call again with changed pet names upserts the existing rows in place, exactly
like the top-level structs. Both verbs are write-only operations: children absent from the
attrs are never deleted or nilified, at any level (unlike `cast_assoc/3`'s `:on_replace`
behavior during regular `Repo.insert/update` calls).

## The other association kinds

`has_one` and `many_to_many` associations work the same way: cast them in the changeset and
include them in the attrs.

- `has_many` / `has_one`: each child must carry its own foreign key (as shown above with
  `person_id`).
- `many_to_many`: the associated records and the join table rows are both written for you, and
  duplicate records and links are removed automatically. With `insert/4`, a pre-existing
  shared child or join row raises by default — see the join-table recipe in
  [Recipes](recipes.html).
- `embeds_one` / `embeds_many`: embedded schemas have no table of their own, so they are
  stored inline on the parent row.

Nesting works recursively at any depth — a child's own associations (e.g. the pets' vet
appointments) are written the same way.

## Known limitations

Nested `belongs_to` associations are not written. To associate with a `belongs_to` parent,
include its foreign key field in the attrs (e.g. `category_id`). This applies at every level
of nesting.