2026-01-17 · Engineering
Ecto: Recursively Load Self-Referential Associations
Originally published June 2016 on tensiondriven.github.io
I have a project that has a table of questions, and each question belongs_to another question, creating a nested tree hierarchy. There are some cases where I want to use Ecto to load all parent questions or child questions.
Retrieving recursively nested associations isn't easily supported using [preload/3](https://hexdocs.pm/ecto/Ecto.Query.html#preload/3), but it can be achieved by adding a couple of methods to your model.
Thanks to @narrowtux for linking me to his example.
In your model
defmodule Model do
schema "models" do
field :foo, :string
has_many :children, Model, foreign_key: :parent_id
belongs_to :parent, Model, foreign_key: :parent_id
end
@doc """
Recursively loads parents into the given struct until it hits nil
"""
def load_parents(parent) do
load_parents(parent, 10)
end
def load_parents(_, limit) when limit < 0, do: raise "Recursion limit reached"
def load_parents(%Model{parent: nil} = parent, _), do: parent
def load_parents(%Model{parent: %Ecto.Association.NotLoaded{}} = parent, limit) do
parent = parent |> Repo.preload(:parent)
Map.update!(parent, :parent, &Model.load_parents(&1, limit - 1))
end
def load_parents(nil, _), do: nil
@doc """
Recursively loads children into the given struct until it hits []
"""
def load_children(model), do: load_children(model, 10)
def load_children(_, limit) when limit < 0, do: raise "Recursion limit reached"
def load_children(%Model{children: %Ecto.Association.NotLoaded{}} = model, limit) do
model = model |> Repo.preload(:children) # maybe include a custom query here to preserve some order
Map.update!(model, :children, fn(list) ->
Enum.map(list, &Model.load_children(&1, limit - 1))
end)
end
end
In your controller
defmodule ModelController do
def show(id) do
model = Repo.get(Model, id)
|> Model.load_parents
|> Model.load_children
# do something
end
end
Reference: Original gist by @narrowtux
Migrated to Outline January 2026