Composable Queries with Ecto

In my previous post I briefly covered some lessons I'd learned while building a (kind of real) web app in Elixir. Today, I'd like to take an in-depth look at composable queries in Ecto. First, a brief introduction to Ecto.

What is Ecto?

I think of Ecto as a light-weight ORM. Ecto uses Elixir structs to represent database tables and provides a DSL for building and executing database queries. Because I'm boring, we're going to use the ages-old "post has many comments" example to demonstrate Ecto's capabilities. We'll assume we have the following models.

defmodule MyApp.Post do  
  use Ecto.Model
  import Ecto.Query

  schema "posts" do
    field :body, :string
    field :published, :boolean
    field :published_at, :datetime
    field :title, :string

    has_many :comments, MyApp.Comment
  end
end  
defmodule MyApp.Comment do  
  use Ecto.Model
  import Ecto.Query

  schema "comments" do
    field :commenter, :string
    field :title, :string
    field :votes, :integer

    belongs_to :post, MyApp.Post
  end
end  

Querying with Ecto

Ecto provides two styles of querying syntax: keyword query syntax and query expressions. Note that in both query styles, the construction of the query is a distinct and separate process from the execution on the query. In each style, a constructed query is passed to the application's Repo to be executed.

Keyword Query Syntax

Keyword query syntax closely mirrors SQL and feels a lot like LINQ. I'll demonstrate using some example queries.

Select all posts

MyApp.Repo.all(  
  from p in MyApp.Post,
  select: p
)

Select all published posts

MyApp.Repo.all(  
  from p in MyApp.Post,
   where: p.published == true,
  select: p
)

Select all comments for post 1

MyApp.Repo.all(  
  from c in MyApp.Comment,
    join: p in assoc(c, :post),
   where: p.id == 1,
  select: c
)

Query Expressions

Query expressions follow the pipeline concept often seen in Elixir APIs. It is important to note that these queries all start with the model module (e.g. MyApp.Post). The model itself is a queryable object that represents all items in the given table. Here are the same examples using query expressions.

Select all posts

MyApp.Post |> MyApp.Repo.all  

Select all published posts

MyApp.Post  
|> where([p], p.published == true)
|> MyApp.Repo.all

Select all comments for post 1

MyApp.Comment  
|> join(:left, [c], p in assoc(c, :post))
|> where([_, p], p.id == 1)
|> select([c, _], c)
|> MyApp.Repo.all

Query Composition

It is easy to see how queries built in the query expression style can be composed - you simply add new constraints to your pipeline. It is not immediately obvious how to compose queries built with the keyword query syntax nor how to compose queries of differing types.

First, we must understand an important feature of the keyword query syntax. In the from clause, the token after the in can be any queryable object and other queries are queryable! Here's an example.

query  = from p in MyApp.Post,  
         select: p

query2 = from p in query,  
         where: p.published == true

MyApp.Repo.all(query2)  

Knowing this, we can now mix and match query syntax types.

query  = from p in MyApp.Post,  
         select: p

query |> where([p], p.published == true) |> MyApp.Repo.all  

Putting It All Together

Now, let's add some functions to our Ecto models with nice, descriptive names.

defmodule MyApp.Post do  
  use Ecto.Model
  import Ecto.Query

  schema "posts" do
    field :body, :string
    field :published, :boolean
    field :published_at, :datetime
    field :title, :string

    has_many :comments, MyApp.Comment
  end

  def published(query) do
    from p in query,
    where: p.published == true
  end

  def sorted(query) do
    from p in query,
    order_by: [desc: p.published_at]
  end
end  
defmodule MyApp.Comment do  
  use Ecto.Model
  import Ecto.Query

  schema "comments" do
    field :commenter, :string
    field :title, :string
    field :votes, :integer

    belongs_to :post, MyApp.Post
  end

  def for_post(query, post) do
    from c in query,
     join: p in assoc(c, :post)
    where: p.id == ^post.id
  end

  def popular(query) do
    query |> where([c], c.votes > 10)
  end
end  

I've used both styles of querying to show their interchangability when it comes to composition. Now, let's use our functions to build some queries. Normally, I'd do this type of composition in my Phoenix controllers.

alias MyApp.Post  
alias MyApp.Comment

published_posts = Post  
|> Post.published
|> MyApp.Repo.all

last_post = Post  
|> Post.published
|> Post.sorted
|> MyApp.Repo.one

recent_popular_comments = Comment  
|> Comment.for_post(last_post)
|> Comment.popular
|> MyApp.Repo.all

Wrap Up

I hope this gives you an idea of the power and flexibility Ecto provides for extracting reusable query components and composing them to build more complex queries. I've found these techniques reduce duplication and complexity while aiding testing.

Update

You can find a talk I gave on this subject at ElixirConf 2015 here.