Writing composable queries in Ecto

In this article, I'll show you how to refactor your Ecto Queries by breaking them up.

This example is from my side project where the below query fetches all the completed todos in the last 30 days for all users.

  def feed() do
    Repo.all from t in Todo,
        join: u in assoc(t, :user),
        where: t.completed == true,
        where: t.inserted_at > type(^DateHelpers.rewind(30), :date),
        preload: [user: u],
        order_by: {:desc, t.inserted_at}
  end

The query worked fine. But I wanted the function to be smaller and more extensible.

I got some inspiration from the docs itself.

Composing queries uses the same syntax as creating a query. The difference is that, instead of passing a schema like User on the right side of in, we passed the query itself.

And I wound up iterating to a place where I feel much happier.

  def completed(query \\ Todo) do
    from t in query, where: t.completed == true
  end

  def last_30_days(query \\ Todo) do
    from t in query, where: t.inserted_at > type(^DateHelpers.rewind(30), :date)
  end

  def completed_in_last_30_days(query \\ Todo) do
    query |> completed |> last_30_days
  end

  def feed() do
    query = from t in Todo.completed_in_last_30_days,
            join: u in assoc(t, :user),
            preload: [user: u],
            order_by: {:desc, t.inserted_at}

    Repo.all(query)
  end

I still see the from t in query, pattern being repeated, but I'll leave that for another day.

Happy Hacking 😊

Show Comments