Posted almost 6 years ago. Visible to the public.

ActiveRecord Features You Should Be Using

Let's assume a Ruby on Rails application “booksandreviews.com” exists with three models:

Copy
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:

Copy
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

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

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

Copy
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:

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

Copy
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:

Copy
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:

Copy
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:

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

Copy
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:

Copy
class Book < ActiveRecord::Base after_commit :enqueue_for_review # ... end

The same code results a much more desirable outcome:

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

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

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

Copy
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:

Copy
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:

Copy
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

Owner of this card:

Avatar
Alexander M
Last edit:
almost 6 years ago
by Alexander M
Posted by Alexander M to Ruby and RoR knowledge base
This website uses short-lived cookies to improve usability.
Accept or learn more