Rails 7 review and Rails 7.1 preview - presentation slides from Ruby User Group Frankfurt

12th February 2023 – 2166 words

I recently had the pleasure of presenting at the Ruby Meetup in Frankfurt! It was fun to prepare and curate all the changes and share my thoughts on the latest Rails 7 release, as well as give a sneak peek into what’s in store for Rails 7.1.

You can view the presentation slides in a interactive Reveal.js presentation on our hackmd scratchpad. As the format for Reveal.js is markdown, it is easy to just copy all the content below, too. So, if that’s interesting for you, grab a cup of coffee (or tea, if that’s your thing) and let’s get started.

Rails 7

  • Released on: December 15, 2021
  • 6.1 will receive (low/medium) Sec-Fixes until 7.1 is released
  • Diff to 6.1:
git diff v6.1.4.1...origin/7-0-stable --shortstat
 2109 files changed, 83193 insertions(+), 36975 deletions(-)

4683 commits by 29 contributors

Attribute-level Encryption

class Person < ApplicationRecord
  encrypts :name
  encrypts :email_address, deterministic: true
end
  • TLDR: declare in model
  • use deterministic, if you need to look up names in SQL (email)
  • can migrate/use unencrypted columns, will transparently encrypt when saving
Content to encrypt Original column size Recommended size
Email addresses string(255) string(510)
Short sequence of emojis string(255) string(1020)
Summary of texts written in non-western alphabets string(500) string(2000)
Arbitrary long text text text

=> ~2x limit sizes of varchar columns to accommodate for Base64 key overhead and key

Guide PR

Strict loading

```ruby [2|3|5-6] class User < ApplicationRecord has_many :bookmarks has_many :articles, strict_loading: true end

user = User.first user.articles # BOOM


Disallow N+1 Loading on a model, attribute or global basis

[PR](https://github.com/rails/rails/issues/41181)


### load_async


```ruby
def index
  @categories = Category.some_complex_scope.load_async
  @posts = Post.some_complex_scope.load_async
end

- @posts.first #-> will block

Parallelize loading of lots of records

Keep in mind:

  • overhead is non neglectable
  • Thread pool size for database connections
  • Memory usage, if loading tons of records still high

PR

In order of


Post.in_order_of(:id, [3, 5, 1])
# SELECT "posts".* FROM "posts"
# ORDER BY CASE "posts"."id"
#   WHEN 3 THEN 1 WHEN 5 THEN 2 WHEN 1 THEN 3
#    ELSE 4 END ASC
Post.in_order_of(:type, %w[Draft Published Archived]).
    order(:created_at)
# ORDER BY
#  FIELD(posts.type, 'Archived', 'Published', 'Draft') DESC,
#  posts.created_at ASC

(e.g. you have the order from another service, Elasticsearch, Machine learning)

PR

scope.excluding(*records)

excludes the specified record (or collection of records) from the resulting relation:

Post.excluding(post)
Post.excluding(post_one, post_two)

class JobAd
  def related_jobs
    organisation.job_ads.excluding(self)
  end
end

PR

where.associated

check for the presence of an association

# Before:
account.users.joins(:contact).where.not(contact_id: nil)

# After:
account.users.where.associated(:contact)

Mirrors existing where.missing.

Enumerating Columns

Before:

Book.limit(5)
# SELECT * FROM books LIMIT 5

After:

Rails.application.config.
    active_record.enumerate_columns_in_select_statements = true
Book.limit(5)
# SELECT title, author FROM books LIMIT 5

Why: Avoid PreparedStatementCacheExpired, consistent column ordering.

PR

virtual columns

PG only:

create_table :users do |t|
  t.string :name
  t.virtual :name_upcased, type: :string,
    as: 'upper(name)', stored: true
end

PR

Unpermitted redirect

Raise error on unpermitted open redirects.

redirect_to @organisation.website_url #OLD # BOOM

redirect_to @organisation.website_url,
  allow_other_host: true # NEW

Commit

Zeitwerk

Zeitwerk Autoloader Mode is default and not changeable.

  • All classes/modules must match file-path, or define exception/collapses in Zeitwerk

PR

Activestorage Variants

Add ability to use pre-defined variants.

class User < ActiveRecord::Base
  has_one_attached :avatar do |attachable|
    attachable.variant :thumb, resize: "100x100"
    attachable.variant :medium, resize: "300x300",
      monochrome: true
  end
end

<%= image_tag user.avatar.variant(:thumb) %>

PR

ActiveStorage misc.

vips is new default, but mini_magick can still be used. PR1:

# default:
config.active_storage.variant_processor = :vips.
# if want to keep imagemagick
config.active_storage.variant_processor = :mini_magick

blob expiration can be set individually per call PR2:

rails_blob_path(user.avatar, disposition: "attachment",
    expires_in: 30.minutes)
  • Previewing video files: FFMpeg args configurable
  • Scene detection for preview
  • Analyzer will determine if the file has audio and/or video data and prefills: metadata[:audio] and metadata[:video]
  • Audio analyzer added: duration + bitrate extracted
  • make image_tag globally lazy loading (free WebVitals®)
    config.action_view.image_loading = "lazy"
    

PR PR PR PR

Rails 7 Asset Story

Sprockets is mostly deprecated now. Webpacker is archived.

New Gems:

  • importmap-rails: Link to JS deps directly to CDN (or vendor), use browser as bundler using modules.
  • propshaft: Successor of sprockets, but without any processing (static files, images)
  • jsbundling-rails: Installation scripts and very thin layer to wrap JS build tools (esbuild, rollup, also Webpacker)
  • cssbundling-rails: Installation scripts for Tailwind, Bootstrap-scss

Upgrade Path

Choices with new apps

Rails 7.1 pre

The “minor” jumps between 4.0 -> 4.1 -> 4.2 etc. took an average of 273 days

-> IMO should be released soon

git diff origin/7-0-stable...origin/main --shortstat
 1735 files changed, 60562 insertions(+), 42135 deletions(-)

3584 commits by 56 contributors

password challenge

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

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

PR

generates_token_for

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

```ruby [2-3|6] class User < ActiveRecordBase generates_token_for :password_reset, expires_in: 15.minutes do BCryptPassword.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


[PR](https://github.com/rails/rails/issues/44189)


### normalizes

[PR](https://github.com/rails/rails/issues/43945)


```ruby
class User < ActiveRecord::Base
  normalizes :email, with: -> email { email.strip.downcase }
end

user = User.create(email: " CRUISE-CONTROL@EXAMPLE.COM\n")
user.email                  # => "cruise-control@example.com"

user = User.find_by(email: "\tCRUISE-CONTROL@EXAMPLE.COM ")

select with hash values

FINALLY, ARRelation#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 })

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: "...")

PR

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

PR

Allow (ERB) templates to set strict locals.

Define, which locales (not controller instance vars) are required by a partial

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

<%= message %>

PR

Find unused routes

rails routes --unused

Tries to find defined routes that either:

  • without a controller or
  • missing action AND missing template (implicit render)

Postgres CTE Support

Post.with(posts_with_comments:
    Post.where("comments_count > ?", 0))

# WITH posts_with_comments AS (
#    SELECT * FROM posts WHERE (comments_count > 0))
#    SELECT * FROM posts

Note: 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:

PR

Limit log size

config.log_file_size = 100.megabytes

No gigantic development.log or test.log anymore! 🎉🍾 (5GB in my case)

PR

rails/docked

Setting up Rails for the first time with all the dependencies necessary can be daunting for beginners. Docked Rails CLI uses a Docker image to make it much easier, requiring only Docker to be installed.

docked rails new weblog
cd weblog
docked rails generate scaffold post title:string body:text
docked rails db:migrate
docked rails server

Improved Docker asset building

SECRET_KEY_BASE_DUMMY can be set to make assets:precompile not compile (Needed for docker)

RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile

PR

Dockerfile (Prod)

Dockerfile .dockerignore entrypoint in newly generated apps

PR

They’re intended as a starting point for a production deploy of the application. Not intended for development

rails/mrsk

MRSK ships zero-downtime deploys of Rails apps packed as containers to any host. It uses the dynamic reverse-proxy Traefik to hold requests while the new application container is started and the old one is wound down. It works across multiple hosts at the same time, using SSHKit to execute commands.

Minor stuff explained here in recent blog post series:

Upgrade strategies

I.
Use Rails Main for production
(Github, Basecamp, Shopify)
II.
Dual Boot with next version
(fastruby/next-rails, (next-rails, shopify/bootboot)
III.
manual, planned upgrade ⬅️/

Manual Upgrade:

  1. Use Bundle 2.4.0+ to get better resolution results/errors (PR - released on christmas) You might also check your Gemfile on RailsBump
  2. Modify Rails version in Gemfile, try bundle update rails ... with all gems that require rails version
  3. Try boot the app, fix errors
  4. use rails app:update, or use railsdiff.org
  5. Fix tests + deprecations
  6. Try enabling migration flags in
     config/initializers/new_framework_defaults_7_0.rb
    
  7. ([much] later) if all enabled, remove file, set
     config.load_defaults 7.0
    

Rails Changelog Explorer

Script/Page by me (with RSS-Feed) https://zealot128.github.io/rails-changelog

Discussion

  1. How frequent to you upgrade your Apps?
  2. How many releases do you stay behind?
  3. Do you use Dual Boot or use Rails main-branch?
  4. What’s your biggest problems for updates?

– masto: @zealot128@ruby.social gh: zealot128, web: stefanwienert.de

Company: pludoni GmbH Job-boards: Empfehlungsbund.de JobsDaheim.de sanosax.de ITsax.de ITbbb.de ITrheinmain.de …