Rails 7.1 preview of new features

1st November 2022 – 1173 words

Rails 7.0 was released almost a year ago (december 2021), so a new Rails release might be just around the corner. Time to look into the CHANGELOG.md of the various Core-Gems. The following features are just a small selection by me of all the Changelog.md’s. There are tons and tons of fixes all around, but like all huge frameworks, not all parts are used by everyone.

I just have been downloading the CHANGELOG.md of the various projects:


%w[actionmailer actioncable activemodel actionpack actionmailbox actiontext actionview activejob activerecord activestorage activesupport railties].each do |gem|
  url = "https://raw.githubusercontent.com/rails/rails/main/#{gem}/CHANGELOG.md"
end

Methodology of getting all the changes.

ActiveRecord, ActiveModel

has_secure_password password_challenge

has_secure_password now supports password challenges via a password_challenge accessor and validation.

Creates a virtual column password_challenge and adds a validation that the password must match when updating the core model itself, IF it is not nil. E.g. User must provide “old” password when updating email/password.

Safes some boilerplate and integrates nicely in the core validation flow.

password_params = params.require(:user).permit(
  :password_challenge, :password, :password_confirmation,
).with_defaults(password_challenge: "")
# Important: MUST not be nil to activate the validation

if current_user.update(password_params)
  # ...
end

ActiveRecord::Base::generates_token_for

Automatically generate and validate various “tokens” for the user, think: Password Reset Token, Login Token etc.

class User < ActiveRecord::Base
  has_secure_password

  generates_token_for :password_reset, expires_in: 15.minutes do
    # A password's BCrypt salt changes when the password is updated.
    # By embedding (part of) the salt in a token, the token will
    # expire when the password is updated.
    BCrypt::Password.new(password_digest).salt[-10..]
  end
end

user = User.first
token = user.generate_token_for(:password_reset)

User.find_by_token_for(:password_reset, token) # => user

user.update!(password: "new password")
User.find_by_token_for(:password_reset, token) # => nil

Infinity Ranges on on Validators:

validates_length_of :first_name, in: ..30
validates_inclusion_of :birth_date, in: -> { (..Date.today) }

Allow ActiveRecord::QueryMethods#select to receive hash values.

FINALLY, select can be used with hash syntax, too.

Post.joins(:comments).
  select(posts: [:id, :title, :created_at], comments: [:id, :body, :author_id])
Post.joins(:comments).
  # also with selection-aliases
  select(posts: { id: :post_id, title: :post_title }, comments: { id: :comment_id, body: :comment_body })

find_or_create_by now try to find a second time if it hits a unicity constraint.

Sound’s useful for some race conditions.

CTE Support

Sometimes, it’s nice to define “Common Table Expressions” which are supported by some databases, to clean up huge SQL. In the past one had to fallback to raw SQL for this, but now it is easier to do it with AREL:

Post.with(posts_with_comments: Post.where("comments_count > ?", 0))
# => ActiveRecord::Relation
# WITH posts_with_comments AS (SELECT * FROM posts WHERE (comments_count > 0)) SELECT * FROM posts

Ignore tables for schema-dump

Nice, if you have a shared database other (legacy?) tables that you don’t care about:

ActiveRecord::SchemaDumper.ignore_tables = [/^_/]

alias_attribute (not new, but TIL)

I read about a fixed bug in alias_attribute, and thus found out about it the first time. Appareantly it is part of ActiveRecord for a long time, but I never stumbled upon it. I can sometimes think of aliasing old columns to better names, esp. when working with legacy schemata.

class Book < ApplicationRecord
  alias_attribute :title, :name
end

User.authenticate_by

Instead of manually loading the user by mail and THEN validating the password:

User.find_by(email: "...")&.authenticate("...")

Use the new method, which is also supposed to be timing-Attack resistant:

User.authenticate_by(email: "...", password: "...")

Composite primary keys

Preliminary work has been merged, that allows Rails to better handle composite primary keys (on a later stage as noted in the PR)

class Developer < ActiveRecord::Base
  query_constraints :company_id, :id
end
developer = Developer.first.update(name: "Bob")
# => UPDATE "developers" SET "name" = 'Bob' WHERE "developers"."company_id" = 1 AND "developers"."id" = 1

ActionPack View, Railties etc.

Trailing slashes in Routes

Force trailing slashes for various URL demands:

get '/test' => "test#index", as: :test, trailing_slash: true

test_path() # => "/test/"

Allow (ERB) templates to set strict locals.

Define, which locales (not controller instance vars) are required by the template

<%# locals: (message:) -%>
<%= message %>

Default values can also be provided:

<%# locals: (message: "Hello, world!") -%>
<%= message %>

Maybe ok, to prevent typos in the variable names.

Find unused routes

rails routes --unused

Tries to find defined routes without a controller and missing action OR missing template (either is needed).

Grep routes in command

rails routes --grep

Not sure why this is needed on a UNIX/Linux system, but ok, if it is there.

Limit log size

config.log_file_size = 100.megabytes

No gigantic (5GB in some cases…) development.log or test.log anymore! Yay!

Rails.cache with options

ActiveSupport::Cache:Store#fetch` now passes an options accessor to the block.

It makes possible to override cache options:

Rails.cache.fetch("3rd-party-token") do |name, options|
  token = fetch_token_from_remote
  # set cache's TTL to match token's TTL
  options.expires_in = token.expires_in
  token
end