Nested Associations
View SourceThe 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 with a
has_many :pets association:
priv/repo/migrations/0002_create_pets.exs
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
endlib/your_project/persons/pet.ex
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
endlib/your_project/persons/person.ex
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
endForeign keys are not inferred
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:
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 withperson_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. Withinsert/4, a pre-existing shared child or join row raises by default — see the join-table recipe in Recipes.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.