Search code examples
elixirphoenix-frameworkecto

Compose Ecto Queries in Elixir


I have created a list of queries from a list of parameters that were passed in from a client:

[
  #Ecto.Query<from v in Video, where: v.brand == ^"Cocacola">,
  #Ecto.Query<from v in Video, where: v.type == ^"can">
]

However, I need to iterate through this list and compose a single query which is the accumulation of all of them.. (the input of the next query is the current query, etc..)

Would someone be able to point me in the right direction about how to do this. It would be greatly appreciated!

I understand I can compose the queries one by one. But I am getting params from the client and have a long list of fields (brands, type, ...etc) and do not want to make a separate query for each one.


Solution

  • Unless you open up the individual query structs and go through their underlying implementation, it is neither possible nor recommended to join queries in Ecto like this. Instead you should try to break them up and make them composable.

    Ecto makes it very easy for you to compose queries together:

    defmodule VideoQueries do
      import Ecto.Query
    
      def with_brand(query, brand) do
        where(query, [v], v.brand == ^brand)
      end
    
      def with_type(query, type) do
        where(query, [v], v.type == ^type)
      end
    
      def latest_first(query) do
        order_by(query, desc: :inserted_at)
      end
    end
    

    And you can call them together like this:

    Video
    |> VideoQueries.with_brand("Cocacola")
    |> VideoQueries.with_type("can")
    |> VideoQueries.latest_first
    



    Now let's say you get a Map or Keyword List of query parameters and you want to apply them, you could still call them together by iterating over the keys/values at runtime. You could build a filter method that does that for you:

    # Assuming args are a Keyword List or a Map with Atom keys
    def filter(query, args) do
      Enum.reduce(args, query, fn {k, v}, query ->
        case k do
          :brand -> with_brand(query, v)
          :type  -> with_type(query, v)
          _      -> query
        end
      end)
    end
    

    And can dynamically compose queries like this:

    user_input = %{brand: "Cocacola", type: "can"}
    
    Video
    |> VideoQueries.filter(user_input)
    |> Repo.all
    



    Further Readings: