Five More Active Record Features You Should Be Using

In a previous post, I illustrated a few helpful ActiveRecord features. The entire API of ActiveRecord cannot possibly be contained within a single or even a handful of digestible posts; but, here are at least five more pieces of that massive API that some might find useful.

Just like before, the example application used to demonstrate these features will be an imaginary Ruby on Rails application: “booksandreviews.com”:

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. pluck

Introduced in Ruby on Rails 4.0, the pluck method helps keep memory allocation to a minimum when returning results from ActiveRecord queries. A great use case for the pluck method is when a database table backing an ActiveRecord object has a very large number of columns. In this situation, returning an object’s full dataset can be cause unnecessary memory allocation and potentially expensive deserialization (in the case of custom or JSON serialized columns).

To get a list of book_ids from the 'fantasy' genre, the pluck method is a simple to use:

Book.where(genre: 'fantasy').pluck(:id)
# SELECT "books"."id" FROM "books" WHERE "books"."genre" = 'fantasy'
=> [1, 3, 45, ...]

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']]

While possible, it might not always be the best idea to request multiple columns in a single pluck call. Too many columns can produce an unruly result and nullify the performance gain pluck provided in the first place.

2. 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:

book = Book.find(1)
book.reviews.each do |review|
  review.meaningful_update!
end

If the meaningful_update! method throws an exception, all reviews before the erroring review will update appropriately and all after will not. If this method has a compounding side-effect (like incrementing a field), iterating through all reviews again would be harmful.

With a transaction, this problem does not exist:

ActiveRecord::Base.transaction do
  book = Book.find(1)
  book.reviews.each do |review|
    review.meaningful_update!
  end
end

After adding the transaction, this new 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.

3. after_commit

Love them or hate them, callbacks in Ruby on Rails are a viable option to solve many problems. ActiveRecord callbacks are initiated at different times depending on the operation a model is about to undergo.

A fairly common use case for callbacks in Ruby on Rails revolves around what to do after a model is persisted. A model can be persisted by either save, create or update. Since the concept of “saving” is the common denominator for all these methods, it would be reasonable to use the after_save to trigger logic that should follow one of these persistence operations.

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. The Logger class simply outputs text to stdout.

When everything goes well, the callback works beautifully:

Book.create!(
  title: 'A New Book',
  author_id: 3,
  content: 'Blah blah...'
)
#=> Added 4 to ReviewQueue
#=> <Book id: 4, title: 'A New Book' ..>

ReviewQueue.size
#=> 1

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

This code has now generated an element on the ReviewQueue for a non-existent Book; however, if the after_commit callback is used, this problem will fade away.

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. Ruby on Rails provides many more callback hooks besides after_commit.

4. 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 tracking when a record was last viewed. The good people at “booksandreviews.com” need to be sure their Reviews are being seen and want to know which Reviews have not been viewed in a while.

If the Review table has a last_viewed_at column which is of type timestamp, touch can easily update this column in a controller action:

class ReviewsController < ApplicationController
  def show
    @review = Review.find(params[:id])
    @review.touch(:last_viewed_at)
  end
end

A request which calls this controller action will result in the query:

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

Unlike setting other attributes, the touch method invokes an update query right away.

5. 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. Additionally, ActiveRecord exposes a number of per-attribute helper methods to give even more visibility.

Each key in the changes hash maps to a record’s attribute. The value of each key is an Array with two elements: what the attribute value used to be, and what it is now:

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:

book = Book.find(1)
book.title
# => 'A Book'
book.title.object_id
# => 2158279020
book.title = 'A Book'
book.changed?
# => false
book.title.object_id
# => 2158274600

Two “different” strings were set to book.title but since they resolve to the same characters, the object is not changed?.

When using PostgreSQL, the changed? method can save unnecessary begin and commit transaction calls for objects that have no changes:

class BooksController < ApplicationController
  def update
    @book = Book.find(sanitized_params[:id])
    @book.assign_attributes(sanitized_params)
    @book.save! if @book.changed?
  end
end

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

To verify the result, we can check that the ReviewQueue decrements its internal count of reviews pending review:

ReviewQueue.size
#=> 1
review = Review.find(1)
review.status
# => 'Pending'
review.status = 'Approved'
review.save!
# => true
ReviewQueue.size
#=> 0

Note: Additional considerations might need to be made since the save is not guaranteed to persist like described above in the after_commit example.