Should a list function be multiple functions for different ids or a single function that takes multiple?

Hello! Elixir noob here, but I’m really liking it so far. But I’ve run into something that I’m not sure how to express.

I have a few Ecto schemas and some of the schemas reference other schemas. Convention, Talk and Host. A Convention belongs to a single Host and a Talk belongs to a single Convention. The schemas will be a little more complex when I’m done, but that’s a fine example for now.

I’d like to write a function (or functions) in my model that lists all Talks by Convention ID as well as all Talks by Host ID. All the ids are the same type. I’m wondering how I should structure this to make good Elixir and Phoenix code.

I have a few ways I’ve considered it:

Separate functions for each:

def list_talks_by_convention(id) do
   # do the queries
end

# etc.

This one seems the simplest, but the multiple function names feels uglier and harder to keep track of.

A Single Function with Multiple Inputs:

def list_talks(convention_id \\ nil, host_id \\ nil) do
  # do queries
end

The problem here is that I don’t know how to enforce having a single id. This also means the function takes more inputs than it actually requries.

A Single Function that takes a dictionary or one-of type

I actually don’t know what this would look like, but it seems like the best option.

I was also thinking I could have more private methods that do the actual query, but I also don’t know if that’s good elixir code or if bigger functions are better.

I could also be missing another better way to do this, so I could use any advice, but if there’s a canonical way to do this, I’d like to know.

Thanks!

There is a common convention of composing ecto queries, which often looks like a separate module with functions that take a query as the first argument and return a query. This allows you to pipe through them.

Talks.by_speaker(speaker_id)
|> Talks.in_month(10)
|> Talks.at_location(location)
|> Repo.all()
defmodule Talks do
  import Ecto.Query

  def base, do: Talk

  def by_speaker(query \\ base(), speaker_id) do
    from t in query, where: t.speaker_id == speaker_id
  end

  ...
end

If you search Google for composable Ecto queries a few blogs will appear showing this in more detail. It’s also in the OTP book from pragprog with the bees on the front.

There is also a method where you pass in a keyword list or map of options/filters and reduce over them to create the query.

2 Likes

One of the option is to have multiple functions that takes a query, filter and returns a query, so that You can chains them, as in the previous post.

def by_convention(q, id), do: from q in query, where: q.convention_id == ^id
def by_host(q, id), do: from q in query, where: q.host_id == ^id

Talk
|> by_convention(x)
|> by_host(y)
|> Repo.all()

The other is to take a keyword list, and reduce through the different filters.

Like this…

def list_talks_query(criteria) do
  Enum.reduce(criteria, Talk, fn
    {:convention_id, id}, query -> from q in query, where: q.convention_id == ^id
    {:host_id, id}, query -> from q in query, where: q.host_id == ^id
  end)
end
#
Talk.list_talks(convention_id: x, host_id: y) |> Repo.all()

I also like to use criterias like limit, offset, order. order_by etc.

Your functions are not easily composable.

1 Like