Composable Queries with Ecto Part 2

Robert Beene
Echobind
Published in
3 min readNov 21, 2017

In a prior article, we demonstrated how to write composable queries leveraging Ecto. Today, we’re going to refactor our code to make things even cleaner to make for a more maintainable codebase.

Refactor

In our prior example, we made use of pattern matching to filter the visibility of users based on the role of the user making a request. Combined with filters based on user-supplied criteria, we can narrow our result set.

A manager of one company shouldn’t see the users who belong to another company after all — regardless of who/what they ask for.

As users make these API requests, we made calls into our top-level context module Account including not only the params but also the current_user as determined by Guardian.

With the user in-hand, we can call into the User module with the params, the current_user, and that user’s role. This gives us everything we need to filter their view of the universe and get the desired results.

Think of this process as a Venn diagram exercise. Each filter is a new set may reduce our matches.

So how can we improve on this design?

You’ll notice in our previous iteration every filter required two versions of the function. One that successfully pattern matches on the params for the given filter. The other doesn’t and simply returns our query.

This is a good opportunity to refactor.

Background

Remember that parameters coming in from an API call will come in as a map. Let’s leverage some of the power that Map gives us with the function to_list.

iex(1)> %{"foo" => "Hello", "baz" => "world"} 
|> Map.to_list()
[{"baz", "world"}, {"foo", "Hello"}]

By having a list in-hand, we can make use of reduce/3 to iterate and pattern match on our params. Let’s look at how this plays out in a more realistic example.

# retrieve active users who report to manager with ID of 1
# params = %{"manager_id" => 1, "status" => "active"}
defmodule Account.User do def list_users(_user, "admin", params) do
User
|> filter_by_params(params |> Enum.to_list())
end
def filter_by_params(query, params) do
Enum.reduce(params, query, fn
{"company_id", company_id}, query ->
filter_by_company(query, company_id)
{"manager_id", manager_id}, query ->
filter_by_manager(query, manager_id)
{"by_gender", gender}, query ->
filter_by_gender(query, gender)
{"by_state", state}, query ->
filter_by_state(query, state)
{"by_title", title}, query ->
filter_by_title(query, title)
{"with_appointment", appointment_type}, query ->
filter_by_appointment_type(query, appointment_type)
# any value that doesn't match
_, query ->
query
end)
end
def filter_by_company(query, company_id) do
query
|> join(:inner, [u], c in assoc(u, :company))
|> where([_u, ..., company], company.id == ^company_id)
end
end

In this example, we take our list of two-element tuples and reduce over it. As we iterate, we pattern match on the first element to determine which filter to apply. Any filter that doesn’t match simply returns query.

In doing so we can reduce (pun intended) the number of functions required by half.

Another approach is to simply call a filter_dataset function that accepts the two element tuple and pattern matches there. This would mean having a number of filters with the same name, but different implementation based on the pattern. Either way, we come out ahead — all thanks to the power of pattern matching and Map.to_list. Our filter_by_params shrinks as well.

Here’s how that solution would look.

defmodule Account.User do
def list_users(_user, "admin", params) do
User
|> filter_by_params(params |> Map.to_list())
end
def filter_by_params(query, params) do
Enum.reduce(params, query, fn
tuple, query ->
filter_dataset(query, tuple)
end)
end
def filter_dataset(query, {"company_id", company_id}) do
query
|> join(:inner, [u], c in assoc(u, :company))
|> where([_u, ..., company], company.id == ^company_id)
end
# catch all
def filter_dataset(query, _no_match_tuple), do: query
end

Win-win.

Want to see how this works in our demo application? Take a spin with the branch 30-refactor-2.

--

--

Echobind
Echobind

Published in Echobind

What We've Learned As a Team Building Great Software.

Robert Beene
Robert Beene

Written by Robert Beene

Helping mentor developers, working with startups. Rails/Elixir/Python