Querying Across Associations with Named Scopes and Merge

Using ActiveRecord::Relations to Your Advantage

Introduction

You have data, and you want insights. ActiveRecord queries give Ruby developers the power to work with databases without sifting through the complex syntax of SQL statements. However, even with ActiveRecord, a complex query can get out of hand fast. Just like SQL, ActiveRecord will allow us to chain methods to our heart’s content, but, at some point, this starts to affect readability, and we can wind up lost in our own logic.

Scope

A scope consists of the scope call, a name, written as a symbol, a lambda (->) and curly braces containing the logic of our query:

class Shirt < ActiveRecord::Base
scope :red, -> { where(color: 'red') }
scope :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
end

Merge

Merge is another querying method that allows us to combine two ActiveRecord::Relation objects. We can use it where there are common conditions between multiple associations. It will return the intersection of the two conditions like so:

first_relation = User.where(:first_name => ‘Bojack’) 
second_relation = User.where(:last_name => ‘Horseman’)
first_relation.merge(second_relation) #=> object for Bojack Horseman
Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )

Combining Scopes and Merges in Action

Imagine we have a library application that has the following models: Author, Book, and Tag. We can assume an author has many books, and a book will belong to an author. A tag will also have many books, but a book could also have many tags (e.g. A Fault in Our Stars might be #fiction, #YA, and #bildungsroman with a theme of #death or #terminal-illness). This many to many relationship means we’ll need a join table, BookTag. Now we have four models and no association between tags and authors. But what if we want to use an ActiveRecord query to find all the authors in our library who write horror?

class Tag < ActiveRecord::Base
has_many :book_tags
has_many :books, through: :book_tags

scope :with_name, -> (name) { where(name: name) }

end
class Book < ActiveRecord::Base
has_many :book_tags
has_many :tags, through: :book_tags

scope :with_tag, -> (name) { joins(:tags).merge(Tag.with_name(name)) }

end
class Author <ActiveRecord::Base
has_many :books

scope :with_books_with_tag, -> (name) { joins(:books).merge(Book.with_tag(name)).uniq }

end

Conclusion

Readability is important. Can you imagine the SQL statement to achieve the above example? Me neither. Luckily we don’t have to, thanks to the power of ActiveRecord::Relations, named scopes, and .merge. Using these methods, we can sort through massive troves of data with relative ease, and get the insights we need to make our apps useful. Try it out next time to keep things DRY and save future collaborators’ precious time.

Full Stack Web Developer//MFA in Creative Nonfiction

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store