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
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
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)
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
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.
virtual columns
PG only:
create_table :users do |t|
t.string :name
t.virtual :name_upcased, type: :string,
as: 'upper(name)', stored: true
end
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
Zeitwerk
Zeitwerk Autoloader Mode is default and not changeable.
- All classes/modules must match file-path, or define exception/collapses in Zeitwerk
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) %>
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]
andmetadata[:video]
- Audio analyzer added: duration + bitrate extracted
- make
image_tag
globally lazy loading (free WebVitals®)config.action_view.image_loading = "lazy"
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.
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: "...")
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
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 %>
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:
Limit log size
config.log_file_size = 100.megabytes
No gigantic development.log or test.log anymore! 🎉🍾 (5GB in my case)
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
Dockerfile (Prod)
Dockerfile .dockerignore entrypoint in newly generated apps
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:
- Use Bundle 2.4.0+ to get better resolution results/errors (PR - released on christmas) You might also check your Gemfile on RailsBump
- Modify Rails version in Gemfile, try
bundle update rails ...
with all gems that require rails version - Try boot the app, fix errors
- use
rails app:update
, or use railsdiff.org - Fix tests + deprecations
- Try enabling migration flags in
config/initializers/new_framework_defaults_7_0.rb
- ([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
- How frequent to you upgrade your Apps?
- How many releases do you stay behind?
- Do you use Dual Boot or use Rails main-branch?
- 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 …