Emmanuel Hayford
Posted on February 28, 2023
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
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
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
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!
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)
)
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)
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)
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
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
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)
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
Validity Checking for PostgreSQL Indexes
Creating indexes as shown below may lead to an invalid index:
add_index :account, :active, algorithm: :concurrently
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?)
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"
)
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
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)
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
With:
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:
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:
<%# locals: (title: "Default title", comment_count: 0) %>
<h2><%= title %></h2>
<span class="comment-count"><%= comment_count %></span>
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>
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!
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
November 29, 2024