Let's assume a Ruby on Rails application “booksandreviews.com” exists with three models:
class Book < ActiveRecord::Base
belongs_to :author
has_many :reviews
end
class Author < ActiveRecord::Base
has_many :books
end
class Review < ActiveRecord::Base
belongs_to :book
end
1. Nested Queries
Active Record’s where
method returns an instance of ActiveRecord::Relation
. These relations can be passed to other methods to aid in query construction. With the same request, we can save the map
and array creation:
books = Book.where(publish_year: '2015')
# => ActiveRecord::Relation
reviews = Review.where(publish_date: '2015-11-15', book: books).to_a
# SELECT "reviews".* FROM "reviews" WHERE "reviews"."publish_date" = '2015-11-15' AND "reviews"."book_id" IN
# (SELECT "books"."id" FROM "books" WHERE "books"."publish_year" = '2015')
Note: This can reduced to a single query with .joins, but for now we can assume that a nested query is desired.
2. DRY Scopes
class Review < ActiveRecord::Base
belongs_to :book
scope :approved, ->{ where(approved: true) }
end
With .merge
, an existing scope can be used in another Active Record query.
books = Book.where(publish_year: '2015')
.includes(:reviews)
.references(:reviews)
.merge(Review.approved)
.to_a
# => SELECT #long books and reviews column select# FROM "books" LEFT OUTER JOIN "reviews" ON "reviews"."book_id" = "books"."id"
# WHERE "books"."publish_year" = '2015' AND (reviews.approved = 't')
3. where.not
The .not
modifier has been introduced in Active Record 4.0.
books = Book.where.not(publish_year: 2012).to_a
# => SELECT "books".* FROM "books" WHERE (publish_year != '2012')
4. first and take
In Ruby on Rails 4.0+, the .first
method returns the first row after the table has been ordered by its id
. This will work fine for every table that has an id
column. However, if a table does not need an id
column, this method causes a problem. To alleviate that problem, the take
method can be used instead of first
:
Author.where(first_name: 'Bill').take
# => SELECT "authors".* FROM "authors" WHERE "authors"."first_name" = "Bill" LIMIT 1
5. .unscoped
The .unscoped
method removes all existing scopes on an Active Record relation.
authors = Author.unscoped.where(last_name: 'Smith').take(5)
authors.map(&:first_name)
# => ['Frank', 'Frank', 'Jim', 'Frank', 'Frank']
6. pluck
Introduced in Ruby on Rails 4.0, the pluck
method helps keep memory allocation to a minimum when returning results from ActiveRecord
queries. To get a list of book_ids
from the 'fantasy'
genre, the pluck
method is a simple to use. An important thing to recognize about the pluck
method is its return value. Unlike its cousin, select
, the pluck
method does not return an ActiveRecord
instance. Multiple column names may be passed to pluck
and the values of these columns will be returned in a nested Array
. The returned Array
will maintain the order of columns to how they were requested:
Book.where(genre: 'fantasy').pluck(:id, :title)
# SELECT "books"."id", "books"."title" FROM "books" WHERE "books"."genre" = 'fantasy'
=> [[1, 'A Title'], [3, 'Another One']]
7. transaction
An important aspect of relational databases is its atomic behaviour. When creating ActiveRecord
objects, maintaining this aspect is possible through the use of the transaction
method. For instance, if an exception occurs during the update of all of a Book's Reviews
, a transaction
can help mitigate harmful side-effects:
ActiveRecord::Base.transaction do
book = Book.find(1)
book.reviews.each do |review|
review.meaningful_update!
end
end
This code will revert all changes contained within the transaction
if an exception is raised. It is equivalent to updating all records in a single query.
8. after_commit
A fairly common use case for callbacks in Ruby on Rails revolves around what to do after a model is persisted. To add a new Book
to a queue for a Review
after it is saved:
class Book < ActiveRecord::Base
after_save :enqueue_for_review
def enqueue_for_review
ReviewQueue.add(book_id: self.id)
Logger.info("Added #{self.id} to ReviewQueue")
end
end
We can assume that the ReviewQueue
is a key/value storage (Redis or something similar) backed object whose purpose is to place new Books
into a queue for critics to review. When everything goes well, the callback works beautifully however, if this code was wrapped in a transaction
, and that transaction
fails, the Book
will not persist but the element on in the Redis-backed ReviewQueue
remains.
ActiveRecord::Base.transaction do
Book.create!(
title: 'A New Book',
author_id: 3,
content: 'Blah blah...'
)
raise StandardError, 'Something Happened'
end
#=> Added 4 to ReviewQueue
#=> Error 'Something Happened'
ReviewQueue.size
#=> 1
Replacing after_save
with after_commit
:
class Book < ActiveRecord::Base
after_commit :enqueue_for_review
# ...
end
The same code results a much more desirable outcome:
ActiveRecord::Base.transaction do
Book.create!(
title: 'A New Book',
author_id: 3,
content: 'Blah blah...'
)
raise StandardError, 'Something Happened'
end
#=> Error 'Something Happened'
ReviewQueue.size
#=> 0
Awesome, the after_commit
callback is only triggered after the record is persisted to the database, exactly what we wanted.
9. touch
To update a single timestamp column on an ActiveRecord
object, touch
is a great option. This method can accept the column name which should be updated in addition to an object’s :updated_at
(if present). A great time to utilize the touch
method is when we track when a record was last viewed.
class ReviewsController < ApplicationController
def show
@review = Review.find(params[:id])
@review.touch(:last_viewed_at)
end
end
# UPDATE "reviews"
# SET "updated_at" = '2016-03-28 00:43:43.616367',
# "last_viewed_at" = '2016-03-28 00:43:43.616367'
# WHERE "reviews"."id" = 1
10. changes
ActiveRecord
keeps a sizable amount of information about an object while it undergoes updates. A hash, accessible via the changes
method, acts a central location for these updates.
review = Review.find(1)
review.book_id = 5
review.changes
# => {"book_id"=>[4, 5]}
a. changed?
A simple boolean method changed?
is available to determine if anything on the object is different since its retrieval.
review = Review.find(1)
review.book_id = 5
review.changed?
# => true
However, if the same value is set on a model, regardless of the value’s object_id
, changed?
returns false
.
b. (attribute)_was
Accessing the same changes
hash as the changed?
method, <attribute>_was
returns the value of an attribute before it was reassigned:
review = Review.find(1)
review.status = 'Approved'
review.status_was
# => 'Pending'
If a Review's
status moving from "Pending"
to "Approved"
should remove it from an approval queue, the status_was
method can be utilized in a callback:
class Review
before_save :maybe_remove_from_review_queue
def maybe_remove_from_review_queue
if status == 'Approved' &&
status_was == 'Pending'
ReviewQueue.remove(id)
end
end
end