An Overview Of Upcoming Ruby on Rails 7.1 Features Part 1

[ comments ]



Table of contents

Intro

The Ruby on Rails repository has seen a tremendous amount of activity this year, introducing a wealth of new framework features, fixing numerous bugs and implementing countless performance improvements.

With so much happening in the Ruby on Rails repository, it can be challenging to keep up. However, in this series of blog posts, I will zero in on some of the exciting new features and improvements in the upcoming minor release of Rails, version 7.1.

In this series of blog posts, I will be sharing my favourite new features and improvements from the Rails repository throughout 2022. This is the first of a three-part series, so be sure to stay tuned for the next two instalments of a comprehensive overview of the exciting developments in Rails this year. You can follow me on Twitter where I’ll send updates; if you’re not on Twitter you can subscribe to my newsletter if you scroll to the bottom.

If you want to stay up to date on the latest developments in the Ruby on Rails framework, I recommend subscribing to This Week In Rails for weekly issues.

Please be aware that the code samples were taken out of the various pull requests and docs verbatim, I haven’t tested them to guarantee they work as they are. That said, all the features described have been merged.

Let’s get started.

01. Rails 7.0.1 released.

Rails kicked off the year with the release of Rails 7.0.1, which introduced support for Ruby 3.1 along with various bug fixes and documentation improvements.

Later in the year, Rails retired Webpacker, a tool that had served as a bridge to compiled and bundled JavaScript for over five years. In its place, Rails introduced import maps, Turbo and Stimulus as the default options, replacing Webpacker, Turbolinks and UJS.

Rails retiring Webpacker in a tweet.

I vividly remember the excitement that surrounded the news of Webpacker’s retirement. Webpacker had been a source of frustration for many developers. The introduction of ./bin/importmap to pin and unpin NPM packages brought back some joy of working with Rails I reckon.

02. Adding autoload paths to $LOAD_PATH disabled.

Beginning with Rails 7.1, paths managed by the autoloader will no longer be added to $LOAD_PATH. This means that they can no longer be loaded using a manual require call; instead, you can reference the class or module directly. To accomplish this change, the config.add_autoload_paths_to_load_path configuration option will be set to false in Rails 7.1 and later versions.

It is recommended to set config.add_autoload_paths_to_load_path to false in the config/application.rb file when running in :zeitwerk mode. This is because Zeitwerk internally uses absolute paths, and therefore models, controllers, jobs and other files do not need to be in the LOAD_PATH when require_dependency is not used.

By setting config.add_autoload_paths_to_load_path to false, you can prevent Ruby from checking these directories when resolving require calls with relative paths. This can also improve the performance of your application by saving on Bootsnap work and RAM usage as Bootsnap does not need to build an index for these directories.

03. A new #update_attribute! method was added.

Rails added a new ActiveRecord::Persistence#update_attribute!method. This method is similar to update_attribute, but calls save! instead of save.

class Topic < ActiveRecord::Base
  before_save :check_title
  def check_title
    throw(:abort) if title == "abort"
  end
end
topic = Topic.create(title: "Test Title")
# => #
topic.update_attribute!(:title, "Another Title")
# => #
topic.update_attribute!(:title, "abort")
# raises ActiveRecord::RecordNotSaved

ActiveRecord::Persistence#update_attribute! raises ActiveRecord::ActiveRecordError if an attribute is marked as readonly.

04. Dart Sass for Rails released.

Rails released a new dartsass-rails gem that wraps the standalone executable version of the Dart-based Sass for use with Rails 7.

dartsass-rails makes it easy to use Sass stylesheets with Rails and replaces the previously used Ruby Sass, which has been deprecated. With this gem, Rails developers can take advantage of the latest features and improvements in Sass while working with Rails.

In a new Rails app, you can install Dart Sass for Rails by doing:

./bin/bundle add dartsass-rails
./bin/rails dartsass:install

The installer will create a default Sass input file, app/assets/stylesheets/application.scss, where you should import all of your style files to be compiled. When you run rails dartsass:build, this input file will be used to generate the output CSS file, app/assets/builds/application.css which you can then include in your app.

05. A new #stub_const method was added.

A new #stub_const method was added to easily change the value of a constant for the duration of a block, silencing warnings. The implementation is not thread-safe if you have parallel testing enabled though.

# 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 , World::List::Import::LARGE_IMPORT_THRESHOLD = 

In the example above, by using this method instead of setting World::List::Import::LARGE_IMPORT_THRESHOLD to 5000, we can prevent warnings from being thrown and ensure that the original value is restored after the test has finished.

Take note, however, that stubbing a constant can have unintended consequences when used in a multithreaded environment. If multiple threads depend on the same constant and each thread attempts to stub the constant, it can lead to conflicting stubs and unpredictable behaviour. To avoid this issue, it is important to carefully consider the impact of stubbing constants in concurrent threads, such as when running separate test suites in parallel.

06. Improved the error message with a missing association.

Using where.associated with a missing association used to raise a cryptic error message, this has now been improved with a much clearer message. It’s better to show you an example with this one.

For a Post that doesn’t have an association named cars, we’d get something like:

Post.where.associated(:cars).to_a
# => NoMethodError: undefined method `table_name' for nil:NilClass

Now, we’ll be getting

Post.where.associated(:cars).to_a
# => ArgumentError: An association named `:cars` does not
# exist on the model `Post`.

Much better.

07. ActiveRecord::ConnectionPool is now Fiber-safe.

Rails made ActiveRecord::ConnectionPool Fiber-safe. Rails has a lot of thread-centric code and does I/O with databases with threads inherently, this pull request makes it possible to switch how the connection pool is interacted with. For instance, if you use a fiber-based job processor or server like falcon, you should set config.active_support.isolation_level to :fiber, in which case multiple fibers in the same thread will be used to manage connections.

08. Rails 7.0.2 released!

On February 8, 2022, Rails 7.0.2 was released with a patch that included the reversal of a problematic feature, as well as the introduction of the ability to version the database schema based on the Rails version in use. This new feature allows existing applications to continue using their database schemas generated in Rails 6.1, maintaining the same behaviour and ensuring that the production database schema remains consistent.

09. #to_fs(:format) replaced #to_s(:format).

The #to_s(:format) method was deprecated not long ago in favor of #to_formatted_s(:format). #to_formatted_s(:format)’s alias was #to_fs(:format) but this pull request swapped so that #to_formatted_s(:format) became an alias to #to_fs(:format). Why? Because #to_formatted_s(:format) is too long a name for a method that’s used so frequently according to DHH. I concur.

10. Password challenge via has_secure_password added.

Rails now allows a password challenge to be implemented with the same ease as a password confirmation, re-using the same error handling logic in the view, as well as the controller.

This enhances has_secure_password to define a password_challenge accessor and the appropriate validation. When password_challenge is set, the validation checks that it matches the currently persisted password_digest (i.e. password_digest_was).

For example, in the controller, instead of:

password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
)
password_challenge = password_params.delete(:password_challenge)
@password_challenge_failed = !current_user.authenticate(password_challenge)
if !@password_challenge_failed && current_user.update(password_params)
  # do something
end

One could write:

password_params = params.require(:password).permit(
  :password_challenge,
  :password,
  :password_confirmation,
).with_defaults(password_challenge: "")
if current_user.update(password_params)
  # do something
end

I couldn’t describe this pull request in a better way than the author, I grabbed this verbatim from the pull request description. Thanks Jonathan.

11. Saving attachments to a record now returns the blob.

Saving attachments to a record with the #attach method now returns the blob or array of blobs that were attached to the record. This means we can use blob methods directly on the attachment! If the record fails to save, #attach will return false.

@user = User.create!(name: "Josh")
avatar = @user.avatar.attach(params[:avatar]) # => Returned a boolean
# You can now directly call blob methods like so:
avatar.download
avatar.url
avatar.variant(:thumb)

12. audio_tag & video_tag now accept attachments.

Rails extended audio_tag and video_tag to accept Active Storage attachments so instead of:

audio_tag(polymorphic_path(user.audio_file))
video_tag(polymorphic_path(user.video_file))

You can now do:

audio_tag(user.audio_file)
video_tag(user.video_file)

13. #destroy_association_async_batch_size added.

Rails added a new ActiveRecord.destroy_association_async_batch_size that allows developers to specify the maximum number of records that will be destroyed in a single background job when using the dependent: :destroy_async association option. If there are more dependent records than the specified batch size, they will be destroyed in multiple background jobs otherwise they’ll be destroyed in a single background job. This helps to ensure that the destruction of large numbers of records can be handled efficiently without overwhelming the application.

14. Active Record added a new API for general async queries.

In Active Record now, there’s a new API for general async queries. In Rails 7.1 you’ll be able to run aggregate methods as well as all methods returning a single record or anything other than a Relation and find_by_sql asynchronously. So you can do stuff like:

Post.where(published: true).count # => 2
promise = Post.where(published: true).async_count
# => #<:promise status="pending">
promise.value # => 2
# All of these are supported too:
=begin
async_sum
async_minimum
async_maximum
async_average
async_pluck
async_pick
async_find_by_sql
async_count_by_sql
=end

15. CSRF tokens can now be stored outside of sessions.

This pull request allows CSRF tokens to be stored outside of sessions. When sessions are not stored in cookies, it can lead to the creation and constant eviction of millions of sessions solely for the purpose of storing a CSRF token. To address this issue, Rails has added a new configuration option that allows a lambda to be provided that can store the CSRF token in a custom location.

You can also implement custom strategy classes for CSRF token storage:

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

16. Keyword arguments for system tests screenshot helper.

With this pull request, we can now selectively screenshot or dump the HTML of tests with the html: and screenshot: keyword arguments, as opposed to collectively dumping the HTML or screenshots when system tests run.

Here are some concrete examples:

# takes a screenshot, shows it in iTerm, and dumps the HTML
# to a file, and prints paths for both
take_screenshot(html: true, screenshot: "inline")
# dumps the HTML to a file and prints its path
take_screenshot(html: true)
# takes a screenshot, shows it in the terminal and prints it path
take_screenshot(screenshot: "artifact")
# takes a screenshot, prints its path
take_screenshot

17. Encrypted attributes on columns with default values.

Previously, reading encrypted attributes defined on columns with default values would raise an error unless config.active_record.encryption.support_unencrypted_data was enabled because the contents of these columns were not encrypted. These values will now be encrypted at the time of record creation regardless of the config settings.

18. Pattern matching for ActiveModel [=Reverted=].

Update: This PR was reverted. Thanks to zverok_kha for the heads-up. A new gem replaced this feature for now.

Probably the most loved of 2022, this pull request introduces a pattern matching interface for hash patterns in Ruby versions 2.7 and later, enabling users to perform pattern matching on any object that includes the ActiveModel::AttributeMethods module, such as ActiveRecord::Base.

You can do interesting stuff like:

case Current.user
in { superuser: true }
  "Thanks for logging in. You are a superuser."
in { admin: true, name: }
  "Thanks for logging in, admin #{name}!"
in { name: }
  "Welcome, #{name}!"
end

Here’s another example:

class Person
  include ActiveModel::AttributeMethods
  attr_accessor :name
  define_attribute_method :name
end
def greeting_for(person)
  case person
  in { name: "Mary" }
    "Welcome back, Mary!"
  in { name: }
    "Welcome, stranger!"
  end
end
person = Person.new
person.name = "Mary"
greeting_for(person) # => "Welcome back, Mary!"
person = Person.new
person.name = "Bob"
greeting_for(person) # => "Welcome, stranger!"

19. db_runtime added to Active Job instrumentation.

This pull request introduces db_runtime to the notification payload of the perform.active_job event. db_runtime tracks the total time (ms) spent on database queries during job execution, allowing for a better understanding of how a job’s time is spent.

20. Validity check for PostgreSQL indexes added.

Creating indexes this way for example add_index :account, :active, algorithm: :concurrently may result in an invalid index. You can now check if an index is valid:

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

21. Exclusion constraints are now supported.

Although for PostgreSQL only, this extends Active Record’s migration or schema dumping to support PostgreSQL exclusion constraints.

add_exclusion_constraint :invoices,
  "daterange(start_date, end_date) WITH &&",
  using: :gist,
  name: "invoices_date_overlap"
remove_exclusion_constraint :invoices, name: "invoices_date_overlap"

22. Messageverifier gets a urlsafe: initialization option.

The MessageVerifier and MessageEncryptor constructors now accept a :urlsafe option. When enabled, this option ensures that messages use a URL-safe encoding. When you pass urlsafe: true to the initializers, you’ll get URL-safe strings in response.

verifier = ActiveSupport::MessageVerifier.new(urlsafe: true)
message = verifier.generate(data) # => "urlsafe_string"

23. Order DESC for in_batches.

This change adds that ability to call in_batches with the specified ordering.

Post.in_batches(order: :desc).each {}
# Now returns:
# Post Pluck (0.1ms)
# SELECT "posts"."id"
# FROM "posts"
# ORDER BY "posts"."id" DESC LIMIT ?  [["LIMIT", 1000]]

It honours the ordering now when it didn’t before. Should I call this a bug fix or a feature?

24. db:prepare can load the schema of an empty database.

Previously, if a database existed but had not been populated with tables, db:prepare would run all migrations. Now db:prepare will load the schema when an uninitialized database exists and dump schema after migrations.

25. Specify the parent class of a job with a job generator.

Rails added a --parent option to the job generator to specify the parent class of a job. There’s now a superclass option in the job generator.

It’s possible now to do:

bin/rails g job process_payment --parent=payment_job

to get:

class ProcessPaymentJob < PaymentJob
  # your stuff here
end

26. datetime_field gets a new include_seconds option.

According to input elements of type time browsers render time differently if you format time without the “seconds” bit. This PR adds an option to omit the seconds part of the formatted time with include_seconds: false.

So something like this:

datetime_field("user", "born_on", include_seconds: false)

Will now generate:


  id="user_born_on"
  name="user[born_on]"
  type="datetime-local"
  value="2014-05-20T14:35"
/>

27. Rails added support for Common Table Expressions.

You can now build sophisticated queries with Common Table Expressions using the .with query method on models. The .with allows the usage of Active Record relations without the need to manually build Arel::Nodes::As nodes.

28. Pre-defined variants for previews allowed now.

This pull request introduces the capability to use pre-defined variants when calling the preview or representation methods on an attachment. This allows for greater flexibility and customization when working with attachments.

For instance, you can now specify which variant of an attachment you want to use when generating a preview or representation.

class User < ActiveRecord::Base
  has_one_attached :file do attachable
    attachable.variant :thumb, resize_to_limit: [100, 100]
  end
end

And then use the defined variant like so:

<%= image_tag user.file.representation(:thumb) %>

29. routes --unused option to detect dormant routes.

Over time, a Rails app can become slow to boot simply because of how many routes it has. This new option for the routes command can be used to detect routes that are drawn but aren’t valid.

bin/rails routes --unused
Found 2 unused routes:
Prefix Verb URI Pattern    Controller#Action
   one GET  /one(.:format) controller#one
   two GET  /two(.:format) controller#two

30. Templates can now define which locals they accept.

This pull request adds the ability for templates to have required arguments with default values. Before this update, templates would accept any locals as keyword arguments, it’s now possible to define the specific locals that a template will accept with a magic.

This enhancement allows for greater control and customization of template behaviour and functionality.

Previously a partial could look like this:

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

<%= title %> class="-count"><%= _count %>

Now, the same partial dons a simpler look:

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

<%= title %> class="-count"><%= _count %>

How gorgeous.

End of Part I.

And that brings us to the end of part one in the series: This Week In Rails Wrapped: A Summary Of Features Coming To Rails 7.1. This and the other two posts I’m working on are just a scratch on the surface of what’s coming in Rails 7.1. There are countless more features I haven’t covered. It’s important to also mention that I didn’t cover bug fixes and performance improvements.

I believe I can speak for other Rails developers when I say we’re grateful to all the 485 contributors that made this possible.

Special thanks to the This Week In Rails team, namely Greg Molnar, Petrik de Heus, Wojciech Wnętrzak and yours truly for making it easier to track and bring you all of these changes. You can subscribe to This Week In Rails going forward to get weekly updates like these in your email.

Stay tuned for part two.

Powered By ConvertKit
Consider subscribing to my newsletter for a chance to get my upcoming eBook collection of rad Ruby idioms, tips & tricks or follow me on Twitter to explore Ruby, JavaScript and web technologies.


© Emmanuel Hayford 2022

[ comments ]