What's New in Rails 7.1

siaw23

Emmanuel Hayford

Posted on February 28, 2023

What's New in Rails 7.1

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:

config.active_record.async_query_executor
config.active_record.global_executor_concurrency
Enter fullscreen mode Exit fullscreen mode

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:

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

teacher = Teacher.first
Enter fullscreen mode Exit fullscreen mode

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:

class Payment < ActiveRecord::Base
  enum :status, %i[succeeded failed], instance_methods: false
end
Enter fullscreen mode Exit fullscreen mode

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

payment = Payment.first

payment.succeeded?
payment.failed?

payment.succeeded!
payment.failed!
Enter fullscreen mode Exit fullscreen mode

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:

Post.with(
  posts_with_comments: Post.where("comments_count > ?", 0),
  posts_with_tags: Post.where("tags_count > ?", 0)
)
Enter fullscreen mode Exit fullscreen mode

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:

Customer.where(id: 1).joins(:orders).explain(:analyze, :verbose)
Enter fullscreen mode Exit fullscreen mode

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:

 Post.group(:title)..unscope(:group).group(:author)
Enter fullscreen mode Exit fullscreen mode

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:

# 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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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:

@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)
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

Validity Checking for PostgreSQL Indexes

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

add_index :account, :active, algorithm: :concurrently
Enter fullscreen mode Exit fullscreen mode

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

connection.index_exists?(:users, :email, valid: true)
connection.indexes(:users).select(&:valid?)
Enter fullscreen mode Exit fullscreen mode

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:

# 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"
)
Enter fullscreen mode Exit fullscreen mode

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:

if ENV["RAILS_ENV"] == "production"
  worker_count = ENV.fetch("WEB_CONCURRENCY") { Concurrent.physical_processor_count }
  workers worker_count if worker_count > 1
end
Enter fullscreen mode Exit fullscreen mode

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:

query.unscope(:eager_load, :preload).group(:id).select(:id)
Enter fullscreen mode Exit fullscreen mode

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:

if Rails.env.development? || Rails.env.test?
end
Enter fullscreen mode Exit fullscreen mode

With:

if Rails.env.local?
end
Enter fullscreen mode Exit fullscreen mode

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:

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
Enter fullscreen mode Exit fullscreen mode

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:

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

<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>
Enter fullscreen mode Exit fullscreen mode

Instead of:

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

<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>
Enter fullscreen mode Exit fullscreen mode

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.

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!

💖 💪 🙅 🚩
siaw23
Emmanuel Hayford

Posted on February 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related