Originally posted on Hint's blog.
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:
If you're not familiar with it, the db:setup
rake task:
- Creates the database
- Loads the
schema.rb
orstructure.sql
file (whichever your application is configured to use) - Runs the
db:seed
task, which executes the code in thedb/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 thattruncate
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:
Post.not_published
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" = ? /* there can be only one! */
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:
@post.comments.touch_all
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 34480 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.
Top comments (3)
Thank you. This was a great writeup!
Great summary, thank you!
👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍👍