Skip to content

Instantly share code, notes, and snippets.

@joshnuss
Last active January 25, 2024 18:59
Show Gist options
  • Star 13 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save joshnuss/9c68ad2c2649b571dd241693dad6f6f6 to your computer and use it in GitHub Desktop.
Save joshnuss/9c68ad2c2649b571dd241693dad6f6f6 to your computer and use it in GitHub Desktop.
Preloading & joining with Ecto, simplified.
# Preloading usually required an extra query.
# To do it in one query, a `join` is needed, and the call to `preload` needs to know the name of join
# This macro does both the `join` and `preload` together
defmodule Preloader do
import Ecto, only: [assoc: 2]
alias Ecto.Query.Builder.{Join, Preload}
defmacro preload_join(query, association) do
expr = quote do: assoc(l, unquote(association))
binding = quote do: [l]
preload_bindings = quote do: [{unquote(association), x}]
preload_expr = quote do: [{unquote(association), x}]
query
|> Join.build(:left, binding, expr, nil, nil, association, nil, nil, __CALLER__)
|> elem(0)
|> Preload.build(preload_bindings, preload_expr, __CALLER__)
end
end
import Ecto.Query
import Preloader
# instead of doing this:
Invoice
|> join(:left, [i], assoc(i, :customer), as: :customer)
|> join(:left, [i], assoc(i, :lines), as: :lines)
|> preload([lines: l, customers: c], lines: l, customer: c)
|> Repo.all()
# you can do this: (exactly the same query)
Invoice
|> preload_join(:customer)
|> preload_join(:lines)
|> Repo.all()
@char0n
Copy link

char0n commented Dec 23, 2019

You can rather use functional composition when building queries. Observe:

  @doc "Fetches video from RDBMS."
  def get_video(id, opts \\ []) do
    base_query = from(video in Video, where: video.id == ^id)

    base_query
    |> filter_not_deleted()
    |> preload_video_metadata(Keyword.get(opts, :preload_video_metadata))
    |> preload_video_channel(Keyword.get(opts, :preload_video_channel))
    |> preload_video_thumbnails(Keyword.get(opts, :preload_video_thumbnails))
    |> Repo.one()
  end

  defp preload_video_channel(queryable, true) do
    from(video in queryable,
      inner_join: video_channel in VideoChannel,
      on: video_channel.id == video.video_channel_id,
      preload: [video_channel: video_channel]
    )
  end

Wouldn't something like that work better for you? This seems like idiomatic solution recommended by various authors. More details about this approach can be found in book Programming Ecto.

@joshnuss
Copy link
Author

joshnuss commented Dec 23, 2019

@char0n functional composition works too, Was just trying to make join + assoc + preload into one reusable thing.

@MMAcode
Copy link

MMAcode commented Dec 9, 2022

Would this work also for nested preloads? If yes, how would that code look like? thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment