ecto-nested-associations
Skill4 starsChanged 4 months agoMITView on GitHub
MANDATORY for ALL nested association and multi-table work. Invoke before writing cast_assoc, cast_embed, Ecto.Multi, or cascade operations. Covers nested creates, updates with on_replace, Ecto.Multi for unrelated tables, on_delete strategies, and FK indexes. Trigger words: cast_assoc, cast_embed, Ecto.Multi, nested, association, cascade, on_delete,
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- apply-phoenix-controller-conventionsSkill
- ash-frameworkSkill
- benchee-profilingSkill
- broadway-data-pipelinesSkill
- cachex-cachingSkill
- code-qualitySkill
- code-reviewSkill
- deployment-gotchasSkill
- ecto-changeset-patternsSkill
- ecto-essentialsSkill
- ecto-migrationSkill
- elixir-essentialsSkill
- gettext-i18nSkill
- liveview-streamsSkill
- mix-tasks-generatorsSkill
- oban-essentialsSkill
- otp-essentialsSkill
- phoenix-authenticationSkill
- phoenix-authorization-patternsSkill
- phoenix-channels-essentialsSkill
- phoenix-json-apiSkill
- phoenix-liveview-essentialsSkill
- phoenix-pubsub-patternsSkill
- phoenix-uploadsSkill
- property-based-testingSkill
- refactor-codeSkill
- req-http-clientSkill
- respond-to-reviewSkill
- security-essentialsSkill
- swoosh-emailsSkill
- telemetry-essentialsSkill
- testing-essentialsSkill
- typespec-dialyzerSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 198
Use a join schema with cast_assoc for full control over nested creation and updates:
- Very specific: line 231
When updating a nested association with only some fields, preload the association first and rely on Ecto's internal ID matching — do not …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Ecto Nested Associations
RULES — Follow these with no exceptions
1. Use cast_assoc/3 for has_many/has_one — never manually insert children in a separate step
2. Use Ecto.Multi for operations spanning multiple unrelated tables — do NOT use Ecto.Multi for nested associations
3. Set on_delete explicitly in migrations — :delete_all for owned children, :nothing for independent entities
4. Always create indexes on foreign key columns
5. Use on_replace: :delete in cast_assoc for list management
6. Preload associations before updating them — cast_assoc compares against currently loaded data
7. Do NOT require foreign keys in child changesets — cast_assoc sets them automatically
8. Use Repo.transaction/1 with Ecto.Multi — wrap multi-table operations for atomicity
End-to-End Workflow
- Identify ownership — determine if children are owned (cascade delete) or independent
- Define schema — add
has_many/belongs_towith appropriateon_replacestrategy - Create migration — add FK column with
on_deleteand create index - Define changesets — child changeset does NOT require FK field; parent uses
cast_assoc - Implement context function — use
Repo.insert/Repo.updatewith parent changeset - Handle results — pattern-match on
{:ok, _}and{:error, changeset} - Write tests — test create, update (including removal), and error cases
cast_assoc for Nested Creates
defmodule MyApp.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
has_many :comments, MyApp.Blog.Comment
timestamps()
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title])
|> validate_required([:title])
|> cast_assoc(:comments, with: &MyApp.Blog.Comment.changeset/2)
end
end
defmodule MyApp.Blog.Comment do
use Ecto.Schema
import Ecto.Changeset
schema "comments" do
field :body, :string
belongs_to :post, MyApp.Blog.Post
timestamps()
end
def changeset(comment, attrs) do
comment
|> cast(attrs, [:body])
|> validate_required([:body])
end
end
# Usage — create post with comments in one operation
Blog.create_post(%{
title: "My Post",
comments: [
%{body: "First comment"},
%{body: "Second comment"}
]
})Handling cast_assoc Failures
case Repo.insert(Post.changeset(%Post{}, attrs)) do
{:ok, post} ->
{:ok, post}
{:error, changeset} ->
# Top-level errors on changeset.errors
# Nested errors on changeset.changes[:comments] (list of changesets)
{:error, changeset}
endcast_assoc for Updates with on_replace
defmodule MyApp.Recipes.Recipe do
schema "recipes" do
field :name, :string
has_many :ingredients, MyApp.Recipes.Ingredient, on_replace: :delete
timestamps()
end
def changeset(recipe, attrs) do
recipe
|> cast(attrs, [:name])
|> validate_required([:name])
|> cast_assoc(:ingredients, with: &MyApp.Recipes.Ingredient.changeset/2)
end
end
# Update — send the full list; omitted items are deleted
def update_recipe(recipe, attrs) do
recipe
|> Repo.preload(:ingredients)
|> Recipe.changeset(attrs)
|> Repo.update()
endEcto.Multi for Unrelated Tables
def create_order_with_payment(order_attrs, payment_attrs) do
Ecto.Multi.new()
|> Ecto.Multi.insert(:order, Order.changeset(%Order{}, order_attrs))
|> Ecto.Multi.insert(:payment, fn %{order: order} ->
Payment.changeset(%Payment{}, Map.put(payment_attrs, :order_id, order.id))
end)
|> Repo.transaction()
endHandling Ecto.Multi Results
case create_order_with_payment(order_attrs, payment_attrs) do
{:ok, %{order: order, payment: payment}} ->
{:ok, order}
{:error, failed_operation, failed_changeset, _changes_so_far} ->
Logger.error("Multi failed at #{failed_operation}: #{inspect(failed_changeset.errors)}")
{:error, failed_changeset}
endon_delete Strategies
defmodule MyApp.Repo.Migrations.CreateComments do
use Ecto.Migration
def change do
create table(:comments) do
add :body, :text
add :post_id, references(:posts, on_delete: :delete_all)
timestamps()
end
create index(:comments, [:post_id])
end
endVerifying FK Indexes After Migration
Confirm FK indexes exist in psql with \d comments. Expect an entry such as comments_post_id_index. If missing, add it in a new migration:
def change do
create index(:comments, [:post_id])
endMany-to-Many Associations
Use a join schema with cast_assoc for full control over nested creation and updates:
# Schema
schema "posts" do
field :title, :string
many_to_many :tags, MyApp.Blog.Tag, join_through: MyApp.Blog.PostTag, on_replace: :delete
timestamps()
end
# Join schema
defmodule MyApp.Blog.PostTag do
use Ecto.Schema
schema "post_tags" do
belongs_to :post, MyApp.Blog.Post
belongs_to :tag, MyApp.Blog.Tag
timestamps()
end
end
# Parent changeset — use cast_assoc with the join schema
def changeset(post, attrs) do
post
|> cast(attrs, [:title])
|> validate_required([:title])
|> cast_assoc(:post_tags, with: &PostTag.changeset/2)
endNested Update with Partial Data
When updating a nested association with only some fields, preload the association first and rely on Ecto's internal ID matching — do not require :id in the child changeset:
def update_post(post, %{post: post_attrs, comments: comments_attrs}) do
post
|> Repo.preload(:comments)
|> Post.changeset(%{post_attrs | comments: comments_attrs})
|> Repo.update()
endCommon Pitfalls
| ❌ Don't | ✅ Do |
| Manually insert children in a separate step | Use cast_assoc/3 for has_many/has_one |
Use Ecto.Multi for nested associations | Use cast_assoc; reserve Ecto.Multi for unrelated tables |
| Build the update changeset without preloading | Repo.preload/2 before cast_assoc compares data |
| Require the FK in the child changeset | Let cast_assoc set the FK automatically |
Omit on_replace on a managed list | Set on_replace: :delete to remove omitted items |
Leave on_delete unset in the migration | :delete_all for owned children, :nothing for independent |
| Forget the foreign key index | create index(:comments, [:post_id]) |
Integration
| Predecessor | This Skill | Successor |
| ecto-changeset-patterns | ecto-nested-associations | testing-essentials |
| ecto-essentials | ecto-nested-associations | apply-ecto-conventions |
Companion skills:
ecto-essentials— schema, migration, and association basicsecto-changeset-patterns— changeset composition andcast_assocrulesecto-migration— migration planning for FK andon_deletechangestesting-essentials— testing nested creates, updates, and error cases
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

