ActiveRecord Features You Should Be Using

Posted Almost 8 years ago. Visible to the public.

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
Alexander M
Last edit
Almost 8 years ago
Alexander M
Posted by Alexander M to Ruby and RoR knowledge base (2016-04-17 11:31)