<style> #doc.slides pre.part { text-align: left; } #doc.slides ol, #doc.slides ul { text-align: left;} .fragment { text-align: left; } table td, table th { font-size: 1.4rem; } a[href*='/issues'],a[href*='/commit'],a[href*='/pull'] { position: absolute; right: 0; top: 0; font-size: 0.9em; background: #ddd7; border-radius: 3px; } .ignore-pr a { position: static; } .ignore-pr, .ignore-pr p { display: flex; flex-direction: row; gap: 5px; justify-content: space-around; } .reveal blockquote { width: 100%; font-size: 0.6em; line-height: 1.3; } h1, h2, h3, h5, h6 { font-family: "PT Serif", "Source Sans Pro", Helvetica, sans-serif !important; } p code, li code { font-size: 0.7em; display: inline-block; background: #eee; padding: 2px; border-radius: 3px; border: 1px solid #ccc; } .has-dark-background code { background: #333; } .has-dark-background a { color: #ddf; } </style> # Rails 7 & 7.1pre Features Stefan Wienert 2023-01-25 ---- ## My Rails history - Rails Dev since Rails 2.3/Ruby 1.8.7 - Same Company for 12 years, running ~20 Rails-apps in prod. - Oldest started from 3.0 - 6 on 7.0, 12 on 6.1 ---- <!-- .slide: data-background="#bd9adb" --> ## Schedule: 1. Rails 7 2. New Asset Pipeline® 3. Rails 7.1pre 4. Upgrade Strategies/Discussion Note: Disclaimer: there will be a lot of slides with new features. Some things are "nice" but not immediately useful. I also made a selection of features I use/happy to have. There are many more PR/commits that I don't include, as well as tons of fixes ---- ## Questions to the Audience <ul> <li class='fragment'> Who has at least one production level app on Rails? </li> <li class='fragment'> Who has at least one production level app on < Rails 6 (5.x 4.x 3)</li> <li class='fragment'> Who knows, what "Dual Booting Rails" means?</li> <li class='fragment'> Who uses it?</li> </ul> --- <!-- .slide: data-background="#1A53aE" --> ## Rails 7 - Released on: December 15, 2021 - 6.1 will receive (low/medium) Sec-Fixes until 7.1 is released - [Diff to 6.1](https://github.com/rails/rails/compare/v6.1.4.1...7-0-stable): ``` 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 ```ruby 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 | <small>=> ~2x limit sizes of varchar columns to accommodate for Base64 key overhead and key</small> [:arrow_right:Guide](https://edgeguides.rubyonrails.org/active_record_encryption.html) [PR](https://github.com/rails/rails/issues/41833) ---- ### 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](https://github.com/rails/rails/issues/41372) ---- ### In order of ```ruby 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](https://github.com/rails/rails/pull/42061) ---- ### `scope.excluding(*records)` excludes the specified record (or collection of records) from the resulting relation: ```ruby Post.excluding(post) Post.excluding(post_one, post_two) class JobAd def related_jobs organisation.job_ads.excluding(self) end end ``` [PR](https://github.com/rails/rails/issues/41439) ---- ### where.associated check for the presence of an association ```ruby # Before: account.users.joins(:contact).where.not(contact_id: nil) # After: account.users.where.associated(:contact) ``` Mirrors existing ``where.missing``. ---- ### Enumerating Columns <small>Before:</small> ```ruby Book.limit(5) # SELECT * FROM books LIMIT 5 ``` <small>After:</small> ```ruby 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](https://github.com/rails/rails/issues/41718) ---- ### virtual columns PG only: ```ruby create_table :users do |t| t.string :name t.virtual :name_upcased, type: :string, as: 'upper(name)', stored: true end ``` [PR](https://github.com/rails/rails/issues/41856) ---- ### Unpermitted redirect Raise error on unpermitted open redirects. ```ruby redirect_to @organisation.website_url #OLD # BOOM redirect_to @organisation.website_url, allow_other_host: true # NEW ``` [Commit](https://github.com/rails/rails/commit/5e93cff83599833380b4cb3d99c020b5efc7dd96) ---- ### Zeitwerk Zeitwerk Autoloader Mode is default and not changeable. :arrow_right: All classes/modules must match file-path, or define exception/collapses in Zeitwerk [PR](https://github.com/rails/rails/issues/43097) ---- ### Activestorage Variants Add ability to use pre-defined variants. ```ruby 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](https://github.com/rails/rails/issues/39135) ---- ### ActiveStorage misc. vips is new default, but `mini_magick` can still be used. [PR1](https://github.com/rails/rails/issues/42790): ```ruby # 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 <a href="https://github.com/rails/rails/issues/42410" style='right: 3ch'>PR2</a>: ```ruby 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" ``` <div class='ignore-pr'> [PR](https://github.com/rails/rails/issues/42471) [PR](https://github.com/rails/rails/issues/42790) [PR](https://github.com/rails/rails/pull/42425) [PR](https://github.com/rails/rails/issues/40096) </div> --- <!-- .slide: data-background="#1A53aE" --> ### Asset Story ``Sprockets`` is mostly deprecated now. ``Webpacker`` is archived. ---- New Gems: - **[importmap-rails](https://github.com/rails/importmap-rails)**: <small>Link to JS deps directly to CDN (or vendor), use browser as bundler using modules.</small> - **[propshaft](https://github.com/rails/propshaft)**: <small>Successor of sprockets, but without any processing (static files, images)</small> - **[jsbundling-rails](https://github.com/rails/jsbundling-rails)**: <small>Installation scripts and very thin layer to wrap JS build tools (esbuild, rollup, also Webpacker)</small> - **[cssbundling-rails](https://github.com/rails/cssbundling-rails/)**: <small>Installation scripts for Tailwind, Bootstrap-scss</small> ---- ### Upgrade: ```flow st=>start: Rails 6 Upgrade wb=>condition: Using Webpacker? sprockets=>condition: Sprockets (scss,coffee, es6,erb)? sh=>end: Keep using or switch to shakapacker if newer Webpack requrired propshaft=>end: Propshaft pech=>end: Keep using sprockets for now Plan migration st->wb wb(yes)->sh wb(no)->sprockets sprockets(no@Only Static)->propshaft sprockets(yes@Have assets)->pech ``` ---- ```flow st=>start: New Rails 7 app simple=>condition: Simple / only Turbo/Stimulus importmaps=>end: Use Importmaps managetools=>condition: Want to manage bundler myself jsbundling=>end: Use JSbundling to integrate your tool (esbuild) vitewebpack=>end: Use Shakapacker or Vite(!) st->simple simple(yes)->importmaps simple(no)->managetools managetools(yes)->jsbundling managetools(no)->vitewebpack ``` --- ## Rails 7.1 pre > <small>The “minor” jumps between 4.0 -> 4.1 -> 4.2 etc. took an average of 273 days</small> -> IMO should be released soon :timer_clock: ``` git diff origin/7-0-stable...origin/main --shortstat 1735 files changed, 60562 insertions(+), 42135 deletions(-) 3584 commits by 56 contributors ``` <!-- .slide: data-background="#1A53aE" --> ---- ### password challenge ```ruby 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](https://github.com/rails/rails/issues/43688) ---- ### 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 < ActiveRecord::Base generates_token_for :password_reset, expires_in: 15.minutes do 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 ``` [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. ```ruby 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: ```ruby User.find_by(email: "...")&.authenticate("...") ``` Use the new method, which is also supposed to be timing-Attack resistant: ```ruby User.authenticate_by(email: "...", password: "...") ``` [PR](https://github.com/rails/rails/issues/43880) ---- ### 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) ```ruby 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](https://github.com/rails/rails/pull/46331) ---- ### Allow (ERB) templates to set strict locals. Define, which locales (not controller instance vars) are required by a partial ```html <%# locals: (message:) -%> <!-- default --> <%# locals: (message: "Hello, world!") -%> <%= message %> ``` [PR](https://github.com/rails/rails/issues/45727) ---- ### Find unused routes ```bash rails routes --unused ``` Tries to find defined routes that either: - without a controller or - missing action AND missing template (implicit render) ---- ### Postgres CTE Support ```ruby 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](https://github.com/rails/rails/issues/45701) ---- ### Limit log size ``` config.log_file_size = 100.megabytes ``` No gigantic development.log or test.log anymore! 🎉🍾 <small>(5GB in my case)</small> [PR](https://github.com/rails/rails/issues/44888) ---- #### "ActiveDeployment" ### [rails/docked](https://github.com/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. ```bash docked rails new weblog cd weblog docked rails generate scaffold post title:string body:text docked rails db:migrate docked rails server ``` ---- #### "ActiveDeployment" ### Improved Docker asset building SECRET_KEY_BASE_DUMMY can be set to make assets:precompile not compile (Needed for docker) ```dockerfile RUN SECRET_KEY_BASE_DUMMY=1 bundle exec rails assets:precompile ``` [PR](https://github.com/rails/rails/pull/46760) ---- #### "ActiveDeployment" ### Dockerfile (Prod) Dockerfile .dockerignore entrypoint in newly generated apps [PR](https://github.com/rails/rails/pull/46762) > They're intended as a starting point for a production deploy of the application. Not intended for development ---- #### "ActiveDeployment" ### [rails/mrsk](https://github.com/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](https://manny.codes/this-week-in-rails-wrapped-an-overview-of-rails-7-1-features-part-i/): <iframe border='0' style='border:0;height: 80vh;width: 100vw' src='https://manny.codes/this-week-in-rails-wrapped-an-overview-of-rails-7-1-features-part-i/'> </iframe> --- ### Upgrade strategies <!-- .slide: data-background="#1A53aE" --> <div style='text-align: left'> I. : Use Rails Main for production <br><small>(Github, Basecamp, Shopify)</small> II. : Dual Boot with next version <br><small>([fastruby/next-rails](https://github.com/fastruby/next_rails), ([next-rails](https://github.com/fastruby/next_rails), [shopify/bootboot](https://github.com/shopify/bootboot))</small> III. : manual, planned upgrade :arrow_left: </div> ---- Manual Upgrade: <div> 0. Use Bundle 2.4.0+ <small>to get better resolution results/errors ([PR](https://github.com/rubygems/rubygems/pull/5960) - released on christmas)</small> 1. Modify Rails version in Gemfile, <small>try ``bundle update rails ...`` with all gems that require rails version </small> </div> 3. Try boot the app, fix errors 4. use ``rails app:update``, or use [railsdiff.org](https://railsdiff.org/) 5. Fix tests + deprecations <!-- .element: class="fragment" data-fragment-index="1" style="width: 100%" --> ---- 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 <small>Script/Page by me (with RSS-Feed) https://zealot128.github.io/rails-changelog</small> <iframe border='0' style='border:0;height: 80vh;width: 100vw' src='https://zealot128.github.io/rails-changelog/versions/7-0'> </iframe> --- ## 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](https://ruby.social/web/@zealot128) gh: [zealot128](https://github.com/zealot128), web: [stefanwienert.de](https://www.stefanwienert.de) Company: pludoni GmbH <small>Job-boards: Empfehlungsbund.de JobsDaheim.de sanosax.de ITsax.de ITbbb.de ITrheinmain.de ...</small>
{"type":"slide","tags":"presentation, rails","slideOptions":{"transition":"slide","theme":"white","allottedMinutes":30}}