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