Scoped Active Record Associations
Active Record Associations are a great feature of Rails. Associations allow you to declare - well - associations between your models. AR Associations also allow you to write operations when several models are linked (e.g.,
Author.first.books.create(title: 'I love Rails!')).
But I had never thought of using them as scopes until last week! So let me show you a neat little trick that’ll make your code much much more expressive (and keep your N+1 queries in check).
Defining basic associations
I won’t dig into the basics of Active Record Associations. If you don’t know your way around them, go and read the doc first.
Let’s define a couple of models with their associations.
Okay, so now we have a one-to-many relationship between an author and its books. In my console, I can query all the books from one author like this:
It will return a collection of books for my first author.
Filtering things out with scopes
Now, let’s say some books can either be available or not available. What I’d like is to get a collection of all the books that are currently available in my application.
How can I filter out unavailable books?
I could write something like this:
But what if I write this line above in multiple places in my code? Well, if I decided to change the condition of the collection, I would need to edit my code several times. This would be time-consuming, and error-prone.
To DRY things up, I can write a scope in my
Book model to centralize the
where(available: true) bit.
available scope now lets me do this:
It looks already more readable, right?
One caveat with scopes is that they are not preloaded. This’ll eventually results in N+1 queries.
What you see above are N+1 queries: one query for the author, one query for each book. Not something you want to keep around in your code.
Let’s make our code more expressive and robust!
I can define a new association with the
available scope as a parameter.
Please note that I’m passing:
- the name of the scope -
available- in the association lambda
- the name of the class the association points to, with the key
Now, I can query available books like this:
It reads like plain English. Beautiful, isn’t it?
I also like that the model
Author doesn’t get to know about the internal logic of books’ availability. This logic is encapsulated in the
Book model because it only concerns books. Neat!
Scoped many-to-many associations
What if our books have several authors? How do we scope through the
Let’s edit our models.
We changed the nature of the association in
Book and added a third table to handle the relationship:
AuthorsBooks. Let’s re-create the
has_many :available_books association.
Like for the
belongs_to association, we passed the name of the scope in a lambda. Only this time, we send it through the
authors_books table. Note that we’re not using
class_name anymore. We’re using
source instead as per Rails documentation.
And what about preloading?
includes, I can preload my scoped association. No more N+1 queries: one query for the author and one query for all the books. Note that, in my loop, I can replace
author.available_books for more readability.
I hope it’ll help next time you need to access a scope through a
Did I miss something? Submit an edit on GitHub.
Rémi - @mercier_remi