Mastering Arel

13 May 2020 at 11:36 - 4 minute read

Any Rails developer can tell you that ActiveRecord is a key component of Rails. It's one of the libraries you'll interact with most frequently, if not the most. It's the ORM for interacting with your database. Basic usage looks something like this:

published_posts = Post.where(published: true)

which executes a query like

SELECT * from posts WHERE published = true

which will return a subclass of ActiveRecord::Relation, which you can more or less operate on with standard enumerable methods (using each, for example). Behind the scenes, ActiveRecord builds an Abstract Syntax Tree (AST) using Arel to represent the query. Basically an AST will haves nodes like AndNode, OrNode, InnerJoin, Equality nodes arranged in a structure that is then visited when constructing the SQL.

So what's Arel and why do I care?

What really matters to us about Arel is why we'd want to use it and how.

You can use arel inside ActiveRecord queries:

Post.where(Post.arel_table[:updated_at].gteq(date))

There are a few things that make Arel useful:

  • composability
  • precision (see gt/lt/gteq/lteq)
  • flexibility

There's a lot of utility in nesting queries at different levels which would be difficult or impossible with just ActiveRecord (do this ad infinitum):

table = Post.arel_table
Post.where(table[:published].eq(true).and(
  table[:updated_at].gt(table[:created_at).or(
    table[:user_id].eq(1)))
  ))

And it becomes easier to do queries with joins while saving parts of a query for later reuse, and queries on multiple tables:

records = Record.arel_table[:validated].eq(true)
status = User.arel_table[:status].in([1,2,3])
reports = Report.arel_table[:completed].eq(true)

Report.joins(:users, :records).where(records.and(status).and(reports))

Using arel is generally preferable to writing clauses with strings. It's more flexible than using hash-type where clauses.

A bit about composability: you might have a filtering system where non-technical users can define the queries (filter on this or that). You want them to be able to use any combination available. So you starts at some table (lets say User) and you want to find their Posts. But you could have started anywhere, so the filter has to do a join and add to the query, without knowing the root table.

class PostFilter
  def joins(model)
    model.path_to(Post)
  end
  def where
    Post.arel_table[:published].eq(true)
  end
end

You'll have another class that glues these filters together (calling joins and where on each of them). The method path_to has to be defined for each model that's usable with this filter - you can have a checker method that checks if it's defined for that model and filter, and if not, disallow its use.

One last bit that might be useful is defining implementation_specific, reusable methods that aren't on ActiveRecord:

# defining a coalesce method and using it to select a field

def coalesce(args)
  Arel::Nodes::NamedFunction.new(
    'COALESCE',
    args
  )
end

User.joins(:settings).select(coalesce([
  User.arel_table[:status],
  Setting.arel_table[:active],
  0]).as('active_status')
)

Note that these selects are really useful for grabbing the data you want, especially when you have a lot of fields that may override others.

Arel gives you power and flexibility while keeping you from having to write many string-based queries that are less maintainable. I hope I've given you a few ideas that may help you going forward.

← JSON Schema and why you should use it