How can I construct better Ecto queries with optional query params?

I’ve been working with Ecto a bit more lately, but I’ve not yet found a good way to do conditional updates on the query passed to repo, when dealing with optional parameters that are passed in. In the simple example below, I maybe be passed the active and sort query parameters, and the following is how I’m handling it, but I’d imagine that there must be a better and more concise way. Your help is appreciated :slight_smile:

def index(conn, params) do
  query = from u in Users

  query =
    case Map.get(params, "active") do
      "true" ->
        Users.active(query)
      _ ->
        query
    end

  query =
    case Map.get(params, "sort") do
      "asc" ->
        User.sort(query, :asc)
      "desc" ->
        User.sort(query, :desc)
      _ ->
        query
    end

  users = Repo.all(query, conn: conn)

  conn
  |> Plug.Conn.put_resp_header("cache-control", "public, max-age=3600")
  |> render("index.json", users: users)
end

In my mind, I was hoping for something like this…

def index(conn, params) do
  query =
    Users
    |> Users.active if Map.get(params, "active") == true
    |> Users.sort(Map.get(params, "sort")) if Map.has_key?(params, "sort")

  users = Repo.all(query, conn: conn)

  conn
  |> Plug.Conn.put_resp_header("cache-control", "public, max-age=3600")
  |> render("index.json", users: users)
end

A consideration that I have is multiple function clauses…

2 Likes

Why not make your User.active take two variables, the first of which is the query, the second is something that is tested as a boolean (and do the same with Users.sort but bail out if nil, then you could just do:

def index(conn, params) do
  query =
    Users
    |> Users.active(Map.get(params, "active", false))
    |> Users.sort(Map.get(params, "sort", nil))

The default is nil regardless so if you handle that case then you could just leave off the default values.

2 Likes

I was thinking you could do something similar to @OvermindDL1 's suggestion, but use pattern matching instead of the boolean parameter:

def active(query, params)
def active(query, %{"active" => active} = _params) do
  query |> where(active: ^active)
end
def active(query, _params) do
  query
end

def sort(query, params, column \\ :inserted_at)
def sort(query, %{"sort" => :asc} = _params) do
  query |> order_by(asc: column)
end
def sort(query, %{"sort" => :desc} = _params, column \\ :inserted_at) do
  query |> order_by(desc: column)
end
def sort(query, _params) do
  query
end

And then you consume it with something like:

query =
  Users
  |> Users.active(params)
  |> Users.sort(params)

You could make the declarations more terse with the , do: syntax probably, but I still use blocks usually (still an Elixir newb).

4 Likes

Indeed, as long as you get it always via the param-map then that would make it very simple. :slight_smile:

1 Like