What's New in Rails 7.1
Tags: ruby, rails
Published: 2023-02-28 15:53:35

Rails 7 was a welcome release that brought a lot of significant features and changes. On the backend, Rails 7 introduced asynchronous query loading and Zeitwerk for autoloading. The frontend saw Hotwire becoming the default solution for new Rails apps.

Rails 7.1 will add to these notable features. In this post, we'll discuss some noteworthy additions that are likely to be shipped.

## A New API for Async Queries in Rails

Building on an earlier feature from Rails 7, Rails 7.1 will make it possible to run some queries asynchronously. Rails 7 introduced `ActiveRecord::Relation#load_async`, which schedules a query in a background thread pool, allowing us to do stuff like `Post.where(published: true).load_async`.

In Rails 7.1, we'll be able to run a lot more queries in background threads. Aggregate methods will run concurrently. Assuming you run two or more independent queries on a job or controller, query results may return quicker if your application is set up accordingly.

For this to work as intended, there are two configuration options worth paying attention to:

```ruby
config.active_record.async_query_executor
config.active_record.global_executor_concurrency
```

Among the methods you can run asynchrously in Rails 7.1 are `async_sum`, `async_pluck`, and `async_count_by_sql`.

## Resetting Singular Associations

Rails 7.1 allows resetting the cache on singular associations. Currently, you can only clear the cache of `has_many` associations with a class like:

```ruby
class Teacher < ActiveRecord::Base
  has_many :students
  has_one :classroom
end

teacher = Teacher.first
```

We can only do `teacher.students.reset` to clear the caches of the results returned by `teacher.students`. Subsequent requests need to hit the database again for a fresh results set in case some data goes stale.

With Rails 7.1, we'll get the `reset` method on a `has_one` association. Using our example class above, Rails 7.1 will allow us to do `teacher.classroom.reset_teacher` to clear the cache for the associations between `teacher` and `classroom`.

## Disabling Methods Generated By `ActiveRecord#enum`

`ActiveRecord#enum` generates a bunch of methods if you create an enum Rails. Rails 7.1 will provide an option to opt out of these generated methods. Here's a simple example:

```ruby
class Payment < ActiveRecord::Base
  enum :status, %i[succeeded failed], instance_methods: false
end
```

Rails won't generate auxiliary methods with `instance_methods: false`. Currently, we expect to have methods like:

```ruby
payment = Payment.first

payment.succeeded?
payment.failed?

payment.succeeded!
payment.failed!
```

## Support for Common Table Expressions

Rails 7.1 will have in-built support for Common Table Expressions (CTEs). This ensures that code will be more succinct but, more importantly, that we won't have to use `Arel::Nodes` for complex queries.

With Rails 7.1, we'll have a new `.with` method to write queries similar to the one below:

```ruby
Post.with(
  posts_with_comments: Post.where("comments_count > ?", 0),
  posts_with_tags: Post.where("tags_count > ?", 0)
)
```

## Support for Async Bulk Record Destruction

As mentioned, Rails 7.1 will introduce several ways to run code asynchronously. One such new addition to async code executions is the `destroy_association_async_batch_size` configuration.

With this new configuration, Rails applications can set a maximum number of records to be destroyed in a single background job by the `dependent: :destroy_async` association.

The default behavior, where all dependent records are destroyed in a single background job when the parent record is destroyed, will remain unchanged. However, if the number of dependent records exceeds the new configuration, they will be destroyed in multiple background jobs.

## Other Rails 7.1 Updates

### `ActiveRecord::Relation#explain` Accepts Options

Rails 7.1 will allow you to pass database systems that support `EXPLAIN` options to `ActiveRecord::Relation#explain`. An example query might look like this:

```ruby
Customer.where(id: 1).joins(:orders).explain(:analyze, :verbose)
```

### Active Record `regroup`

Active Record will allow for "regrouping" queries with a new `regroup` method that can be used like so: `Post.group(:title).regroup(:author)`. This generates SQL equivalent to `SELECT posts.* FROM posts GROUP BY posts.author`.

The same can be achieved in current versions of Rails with more verbose code:

```ruby
 Post.group(:title)..unscope(:group).group(:author)
```

### New `stub_const method` for Testing

A new `stub_const` method for `ActiveSupport::TestCase` will be added that stubs a constant for a yield's duration.

For example:

```ruby
# World::List::Import::LARGE_IMPORT_THRESHOLD = 5000

stub_const(World::List::Import, :LARGE_IMPORT_THRESHOLD, 1) do
  assert_equal 1, World::List::Import::LARGE_IMPORT_THRESHOLD
end

assert_equal 5000, World::List::Import::LARGE_IMPORT_THRESHOLD = 5000
```

Take note, however, that stubbing a constant will affect its value across all threads in a multi-threaded setup. This means that if multiple concurrent threads rely on the same constant, simultaneous and conflicting stubbing may occur.

### Password Challenge via `has_secure_password`

Rails 7.1 has improved the functionality of `has_secure_password` by adding a `password_challenge` accessor and a corresponding validation. The validation will verify that a `password_challenge` matches the stored `password_digest`.

With this, implementing a password challenge becomes as straightforward as a password confirmation. This also enables reusing the same error-handling logic in both the view and the controller.

For instance, instead of writing separate code in the controller, you will simply use the existing logic for password confirmation.

```ruby
password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
).with_defaults(password_challenge: "")

if current_user.update(password_params)
  # perform some work
end
```

### Saving Attachments Returning the Blob

With Rails 7.1, when you save attachments to a record, the `attach` method will return the attached blob or blobs. This enables the direct use of blob methods on the attachment. However, if the record fails to save, `attach` will return `false`.

Here's an example demonstrating its use:

```ruby
@user = User.create!(name: "Josh")
avatar = @user.avatar.attach(params[:avatar])

# You can now directly call blob methods as follows:
avatar.download
avatar.url
avatar.variant(:thumb)
```

### Storage of CSRF Tokens Outside of Sessions

Rails has introduced a new configuration option to address the excessive creation and eviction of millions of sessions for just storing a CSRF token when sessions are not stored in cookies.

This option allows the use of a lambda function to store the CSRF token in a custom location, thus enabling the storage of CSRF tokens outside of sessions.

You can also create custom strategy classes for storing CSRF tokens.

```ruby
class CustomStore
  def fetch(request)
    # Return the token from a custom location
  end

  def store(request, csrf_token)
    # Store the token in a custom location
  end

  def reset(request)
    # Delete the stored session token
  end
end

class ApplicationController < ActionController:x:Base
  protect_from_forgery store: CustomStore.new
end
```

### Validity Checking for PostgreSQL Indexes

Creating indexes as shown below may lead to an invalid index:

```ruby
add_index :account, :active, algorithm: :concurrently
```

With Rails 7.1, you can verify an index's validity as shown here:

```ruby
connection.index_exists?(:users, :email, valid: true)
connection.indexes(:users).select(&:valid?)
```

### `ActiveRecord::QueryMethods#select` Accepts a Hash

`ActiveRecord::QueryMethods#select` in Rails 7.1 now accepts a hash of options. This is best demonstrated with an example:

```ruby
# You can now write selects like this:
Post.joins(:comments).select(
  posts: { id: :post_id, title: :post_title },
  comments: { id: :comment_id, body: :comment_body}
)

# In place of this:
Post.joins(:comments).select(
  "posts.id as post_id, posts.title as post_title,
  comments.id as comment_id, comments.body as comment_body"
)
```

### Number of Processors Match the Puma Worker Count

By default, newly generated Rails applications will have Puma workers that are capped at the total number of physical processors on the host machine. This default setting can always be changed in the `puma.rb` file.

The `puma.rb` file for newly-generated Rails applications will now look like the following:

```ruby
if ENV["RAILS_ENV"] == "production"
  worker_count = ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }
  workers worker_count if worker_count > 1
end
```

### `preload` and `eager_load` Associations to Be Unscoped

Rails 7.1 will add the ability to unscope preloaded and eager loaded associations in a manner similar to how Active Record's `includes`, `select`, and `joins` methods work.

This feature allows for the use of aggregate functions on `has_many` associations previously loaded through `eager_load` or `preload` in existing queries.

An example usage could look like:

```ruby
query.unscope(:eager_load, :preload).group(:id).select(:id)
```

### Default Dockerfiles for New Rails Applications

Docker files are to be added as a default option for new Rails applications. The files include:

- `Dockerfile`
- `.dockerignore`
- `bin/docker-entrypoint`

These files serve as a starting point for deploying an application in a production environment and are not intended for use during development. However, if desired, you can skip these files by using the `--skip-docker` option.

### Default Health Controller

Rails 7.1 introduces a new endpoint for load balancers and uptime monitors. The endpoint, named `Rails::HealthController#show`, is mapped to the "/up" path in newly generated Rails applications. This allows load balancers and uptime monitors to easily track an app's availability.

Note that monitoring the database, Redis, and internal network connections to microservices that an application relies on must be managed separately.

### New `Rails.env.local?` for Environment Checks

In Rails 7.1, a new `local?` method can be used to simplify environment checks.

You'll be able to replace code like:

```ruby
if Rails.env.development? || Rails.env.test?
end
```

With:

```ruby
if Rails.env.local?
end
```

### New `ActiveRecord::Persistence#update_attribute!` Method

Rails has added a new method, `ActiveRecord::Persistence#update_attribute!`, which functions similarly to `update_attribute` but uses `save!` instead of `save`.

Here's how you could use this new method:

```ruby
class Apm < ActiveRecord::Base
  before_save :check_name

  def check_name = throw(:abort) if name == "abort"
end

monitor = Apm.create(name: "App Signal")
# => #<Apm name: "App Signal">

monitor.update_attribute!(:name, "AppSignal")
# => #<Apm name: "AppSignal">

monitor.update_attribute!(:name, "abort")
# raises ActiveRecord::RecordNotSaved
```

### Templates Capable of Defining Accepted Locals

Templates will be enhanced with the option of required arguments that have default values.

Currently, templates accept any locals as keyword arguments. With 7.1, Rails templates will define specific accepted locals through a magic comment.

This improvement provides greater control and customization options for template behavior and functionality.

A partial in Rails could now look like:

```erb
<%# locals: (title: "Default title", comment_count: 0) %>

<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>
```

Instead of:

```erb
<% title = local_assigns[:title] || "Default title" %>
<% comment_count = local_assigns[:comment_count] || 0 %>

<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>
```

## Wrapping Up

As you can see, Rails 7.1 promises a lot of further improvements on Rails 7.

For more information on features, updates, and bug fixes, check out the [Rails 7.1 release notes](https://edgeguides.rubyonrails.org/7_1_release_notes.html).

Happy coding!

**P.S. If you'd like to read Ruby Magic posts as soon as they get off the press, [subscribe to our Ruby Magic newsletter and never miss a single post](https://blog.appsignal.com/ruby-magic)!**