10 New Things in Active Record
Tags: ruby, rails
Published: 2019-11-20 20:14:05

_Originally posted on [Hint's blog](https://hint.io/blog/10-New-Things-in-Active-Record)._


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](https://github.com/rails/rails/pull/35768) by [@robertomiranda](https://github.com/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](https://thepracticaldev.s3.amazonaws.com/i/ad62kdx4mcs0u4g2dzxp.jpg)

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](https://github.com/rails/rails/pull/34779) by [@bogdanvlviv](https://github.com/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](https://github.com/rails/rails/pull/35073) by [@eileencodes](https://github.com/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:

```ruby
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](https://github.com/rails/rails/pull/35381) by [@dhh](https://github.com/dhh)

While [`enum`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/Enum.html) 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:

```ruby
enum status %i(draft published archived)
```

The following scopes have been provided automatically for some time:

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

Now, the following negative scopes are also available:

```ruby
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:

```ruby
Post.not_published
```


## 5. `#extract_associated`

### [PR 35784](https://github.com/rails/rails/pull/35784) by [@dhh](https://github.com/dhh)

The new [`#extract_associated`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/QueryMethods.html#method-i-extract_associated) method is literally just a shorthand for [`preload`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/QueryMethods.html#method-i-preload) plus [`map/collect`](https://rubydocs.org/d/ruby-2-6-5/classes/Enumerable.html#method-i-collect).

Here's the source code of the method:

```ruby
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:

```ruby
commented_posts = user.comments.extract_associated(:post)
```


## 6. `#annotate`

### [PR 35617](https://github.com/rails/rails/pull/35617) by [@mattyoho](https://github.com/mattyoho)

This is a nifty addition that could be used to add useful information to your application's log files. The [`#annotate`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/QueryMethods.html#method-i-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:

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

Would generate the following SQL:

```sql
SELECT "users".*
FROM "users"
WHERE "users"."highlander" = ? /* there can be only one! */
LIMIT ? [["highlander", 1], ["LIMIT", 1]]
```


## 7. `#touch_all`

### [PR 31513](https://github.com/rails/rails/pull/31513) by [@fatkodima](https://github.com/fatkodima)

Another [`ActiveRecord::Relation`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/Relation.html) method, [`#touch_all`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/Relation.html#method-i-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:

```ruby
@post.comments.touch_all
```

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

```ruby
@post.comments.touch_all(:reviewed_at)
```

And, to specify a time value, you would:

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


## 8. `#destroy_by` and `#delete_by`

### [PR 35316](https://github.com/rails/rails/pull/35316) by [@abhaynikam](https://github.com/abhaynikam)

The [`destroy_by`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/Relation.html#method-i-destroy_by) and [`delete_by`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/Relation.html#method-i-delete_by) methods are intended to provide symmetry (["in spirit"](https://github.com/rails/rails/issues/35304#issuecomment-464489289)) with  `ActiveRecord`'s [`find_by`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/FinderMethods.html#method-i-find_by) and [`find_or_create_by`](https://rubydocs.org/d/rails-6-0-0/classes/ActiveRecord/Relation.html#method-i-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:

```ruby
User.find_by(admin: true)
```

Generates the following SQL:

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

Whereas, using `delete_by` with the same parameters:

```ruby
User.delete_by(admin: true)
```

Results in the following SQL:

```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](https://github.com/rails/rails/pull/34906) by [@gregnavis](https://github.com/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:
```ruby
Post.where('num_comments > ?', 10)
```

Now you can use syntax that is more idiomatic Ruby:
```ruby
User.where(num_comments: (10..))
```


## 10. Implicit Ordering

### [PR 34480](https://github.com/rails/rails/pull/34480) by [@tekin](https://github.com/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:
```ruby
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.