Get started with ActiveRecord scoped associations
Active Record Associations are a great feature of Ruby on 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.
The 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!
Adding scopes to your has_one or has_many associations
Remember our available
scope?
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
class_name
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!
Many-to-many associations with scope
What if our books have several authors? How do we scope through the has_many
association?
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?
Thanks to 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.books.available
with author.available_books
for more readability.
I hope it’ll help next time you need to access a scope through a has_many
association!
Did I miss something? Submit an edit on GitHub.
Cheers,
Rémi - @remi@ruby.social