10 New Things in Active Record


Jason Dinsmore - November 20, 2019

In this post, we will take a look at 10 new additions to Active Record in Rails 6. For each addition, I'll include a link to the PR the feature was introduced in, a link to the author's GitHub profile, and a brief description of what the feature provides.

We've got a lot to cover, so let's get going!

1. rails db:prepare

PR 35678 by @robertomiranda

When this rake task is run, if the database exists, it runs any pending migrations. If a database does not exist, it runs the db:setup rake task.

This feature was designed to be idempotent, allowing it to be run over and over again until it completes successfully.

The rake db:prepare task functions like this:

rake db:prepare flowchart

If you're not familiar with it, the db:setup rake task:

  • Creates the database
  • Loads the schema.rb or structure.sql file (whichever your application is configured to use)
  • Runs the db:seed task, which executes the code in the db/seeds.rb file

2. rails db:seed:replant

PR 34779 by @bogdanvlviv

This new db:seed:replant rake task does two things:

  • Runs truncate on all tables managed by ActiveRecord in the Rails environment the rake task is being run under. (Note that truncate deletes all of the data in the table, but does not reset the table's auto-increment (ID) counter)
  • Runs the db:seed Rails rake task to populate seed data

3. Automatic Database Switching

PR 35073 by @eileencodes

Rails 6 provides a framework for auto-routing incoming requests to either the primary database connection, or a read replica.

By default, this new functionality allows your app to automatically route read requests (GET, HEAD) to a read-relica database if it has been at least 2 seconds since the last write request (any request that is not a GET or HEAD request) was made.

The logic that specifies when a read request should be routed to a replica is specified in a resolver class, ActiveRecord::Middleware::DatabaseSelector::Resolver by default, which you would override if you wanted custom behavior.

The middleware also provides a session class, ActiveRecord::Middleware::DatabaseSelector::Resolver::Session that is tasked with keeping track of when the last write request was made. Like the resolver, this class can also be overridden.

To enable the default behavior, you would add the following configuration options to one of your app's environment files - config/environments/production.rb for example:

config.active_record.database_selector = { delay: 2.seconds }
config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver
config.active_record.database_operations = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session

If you decide to override the default functionality, you can use these configuration options to specify the delay you'd like to use, the name of your custom resolver class, and the name of your custom session class, both of which should be descendants of the default classes.

4. Negative Scopes for Enums

PR 35381 by @dhh

While enum has traditionally provided scopes to find items by their enum value, it has not provided scopes to find items not matching a specific enum value.

For example, given a Post model for a blog, with an enum on the status field:

enum status %i(draft published archived)

The following scopes have been provided automatically for some time:

scope :draft, -> { where(status: 0) }
scope :published, -> { where(status: 1) }
scope :archived, -> { where(status: 2) }

Now, the following negative scopes are also available:

scope :not_draft, -> { where.not(status: 0) }
scope :not_published, -> { where.not(status: 1) }
scope :not_archived, -> { where.not(status: 2) }

This makes it easy, for example, to find unpublished posts:

5. #extract_associated

PR 35784 by @dhh

The new #extract_associated method is literally just a shorthand for preload plus map/collect.

Here's the source code of the method:

def extract_associated(association) preload(association).collect(&association)
end

Be aware that preload does not allow you to specify conditions on the association being "preloaded". You'd want to use a different eager-loading mechanism for that, likely includes, eager_load or joins.

Usage of #extract_associated might look like this:

commented_posts = user.comments.extract_associated(:post)

6. #annotate

PR 35617 by @mattyoho

This is a nifty addition that could be used to add useful information to your application's log files. The #annotate method provides a mechanism to embed comments into the SQL generated by ActiveRecord queries. As an added benefit, the comments it generates could be completely dynamic.

Inserting annotate into your query chain like below:

User .annotate('there can be only one!') .find_by(highlander: true)

Would generate the following SQL:

SELECT "users".*
FROM "users"
WHERE "users"."highlander" = ? LIMIT ? [["highlander", 1], ["LIMIT", 1]]

7. #touch_all

PR 31513 by @fatkodima

Another ActiveRecord::Relation method, #touch_all touches all records in the current scope, updating their timestamps.

You can pass an array of columns to touch, and optionally provide a time value to use. touch_all defaults to the current time in whatever timezone the app's config has set for config.active_record.default_timezone (the setting defaults to UTC).

For example, to update the updated_at field of all comments associated with a given blog post, @post, you could:

To update a given field on the comments, say :reviewed_at, you would provide the column name:

@post.comments.touch_all(:reviewed_at)

And, to specify a time value, you would:

@post.comments.touch_all(:reviewed_at, time: the_time)

8. #destroy_by and #delete_by

PR 35316 by @abhaynikam

The destroy_by and delete_by methods are intended to provide symmetry ("in spirit") with ActiveRecord's find_by and find_or_create_by methods.

I believe there is an important distinction you should be aware of. find_by returns one record, or nil, whereas destroy_by and delete_by will match on entire collections of records.

Using find_by like this:

User.find_by(admin: true)

Generates the following SQL:

SELECT "users".*
FROM "users"
WHERE "users"."admin" = $1
LIMIT 1 [["admin", 1]]

Whereas, using delete_by with the same parameters:

User.delete_by(admin: true)

Results in the following SQL:

DELETE FROM "users"
WHERE "users"."admin" = ? [["admin", 1]]

Definitely something to keep in mind when using these methods!

Also, it's worth noting that there are no bang ( !) versions of the delete_by/destroy_by methods.

9. Endless Ranges in #where

PR 34906 by @gregnavis

Ruby 2.6 introduced infinite ranges. This new feature lets you use them in Rails' #where clauses.

For example, when trying to find a Post with more than 10 comments (contrived example, I know).

Before you would have to use SQL like:

Post.where('num_comments > ?', 10)

Now you can use syntax that is more idiomatic Ruby:

User.where(num_comments: (10..))

10. Implicit Ordering

PR 34880 by @tekin

This feature makes implicit ordering configurable for a database table. Rails implicitly orders results by the table's primary key, which can give surprising results when the primary key is something that isn't an auto-incrementing integer (like a UUID).

Setting a table's implicit order column allows you to specify a default order, without using a default scope, meaning that you don't need to use reorder in your code to change the order downstream, you can use order (assuming you have not already specified explicit ordering earlier in the query chain).

For example, if you declare an implicit ordering on your Post table:

class Post < ActiveRecord::Base self.implicit_order_column = 'title'
end

Be aware, that if you declare implicit ordering on a column that does not ensure unique values, your results may not be what you expect.