For more complicated usecase, pattern that i frequently use is to write composeable query with a decent amount of named join. It produce readable code, with nice behavior of join only when necessary.
It looks like
defmodule UserQuery do
def has_minimum_rating(query, rating) do
query = query_require(query, :user_profile)
from(query, [user_profile: up], up.rating > ^ rating)
end
def name_like(query, name) do
query = query_require(query, :user_profile)
from(query, [user_profile: up], ilike(up.name, ^"#{name}*")
end
...
defp query_require(query, identifiers) when is_list(identifiers) do
Enum.reduce(identifiers, query, fn identifier, q ->
query_require(q, identifier)
end)
end
defp query_require(query, identifier) do
if has_named_binding?(query, identifier) do
query
else
implement_query_require(query, identifier)
end
end
defp implement_query_require(query, :user_profile) do
join(query, :left, [u], up in assoc(u, :user_profile), as: :user_profile)
end
end
and then used like
User
|> UserQuery.has_minimum_rating(4)
|> UserQuery.name_like("john")
|> Repo.all()






















