How to upgrade Rails: Workflow advice

When upgrading Rails versions -- especially major versions -- you will run into a lot of unique issues, depending on the exact version, and depending on your app.

However, it is still possible to give some generic advice on how you want to tackle the update in principle.

If you are not really confident about upgrading Rails, have a look at Rails LTS.

How many update steps?

Besides the Rails upgrade itself, you might also want to upgrade your other gems and upgrade your Ruby version.
First decide in how many st...

Rails: Looking up constants by their name string

TL;DR: Rails ships two methods to convert strings to constants, constantize and safe_constantize. Neither is safe for untrusted user input. Before you call either method you must validate the input string against an allowlist. The only difference between the two methods is that unresolvable constants raise an error with constantize, but return nil with safe_constantize. If you validate the input string against an allowlist, an error should never happen.

Preventing Dangerous Lookups

Suppose an application uses eit...

How to: Rails cache for individual rspec tests

Rails default config uses the ActiveSupport::Cache::NullStore and disables controller caching for all environments except production:

config.action_controller.perform_caching = false
config.cache_store = :null_store

If you want to test caching you have at least two possibilities:

  1. Enable caching for every test (not covered by this card and straightforward)
  2. Enable caching for individual test

Enable caching for individual test (file cache)

1. Leave the defau...

Rails: Example on how to extract domain independent code from the `app/models` folder to the `lib/` folder

This cards describes an example with a Github Client on how to keep your Rails application more maintainable by extracting domain independent code from the app/models folder to the lib/ folder. The approach is applicable to arbitrary scenarios and not limited to API clients.

Example

Let's say we have a Rails application that synchronizes its users with the Github API:

.
└── app
    └── models
        ├── user
        │   ├── github_client.rb
        │   └── sychronizer.rb
        └── user.rb

In this example the app folder ...

Organize large I18n dictionary files in Ruby on Rails

If you're suffering from a huge de.yml or similiar file, cry no more. Rails lets you freely organize your dictionary files in config/locales.

My organization works like this:

  • config/locales/rails.de.yml modified Rails boilerplate
  • config/locales/faker.de.yml modified Faker boilerplate
  • config/locales/models.de.yml model names, attribute names, assignable_value labels
  • `config/locales/views.de.y...

Rails: Your index actions probably want strict_loading

By activating strict_loading you force developers to address n+1 queries by preloading all associations used in the index view. Using an association that is not preloaded will raise an ActiveRecord::StrictLoadingViolationError.

I think it's a good default to activate strict_loading in your controllers' #index actions. This way, when a change introduces an n+1 query, you...

Rails: Using PostgreSQL full-text search without a gem

PostgreSQL can cosplay as a full-text search engine. It doesn't have the features or fidelity of ElasticSearch or Algolia, but it's good enough if you just need to search and rank large volumes of text.

This card will teach you how to index, search and rank your Rails models in a PostgreSQL full-text index. We will do this without using any gems aside from ActiveRecord. While there are gems like pg_search or pg_fulltext, manual integration requires very...

Rails: Keeping structure.sql stable between developers

Why Rails has multiple schema formats

When you run migrations, Rails will write your current database schema into db/schema.rb. This file allows to reset the database schema without running migrations, by running rails db:schema:load.

The schema.rb DSL can serialize most common schema properties like tables, columns or indexes. It cannot serialize more advanced database features, like views, procedures, triggers or custom ditionaries. In these cases you must switch to a SQL based schema format:

# in application.rb
config.a...

Rails: Pluck across associated tables

#pluck is commonly used as a performant way to retain single database values from an ActiveRecord::Relation

Book.pluck(:title, :price) #=> [["The Hobbit", "8.99"], ["The Alchemist", "7.89"]]

But #pluck can do more: you can query multiple tables as well!

Book.joins(:author).pluck("books.title, books.price, authors.name") #=> [["The Hobbit", "8.99", "J. R. R. Tolkien"], ["The Alchemist", "7.89", "Paulo Coelho"]]

Note the use of :author for the joins, and then authors for the pluck clause. The first corresp...

Rails: Comparison of assignable_values and Active Record enum types

We are using assignable_values for managing enum values in Rails. Nevertheless Rails is adding more support for enum attributes, allowing to have a closer look at the current feature set in comparison to our still preferred option assignable_values.

Active Record enum attribute interface

By default Rails is mapping enum attributes to integers:
...

Logging multiple lines in Rails without making filtering your logs difficult

Rails' default logger prefixes each log entry with timestamp and tags (like request ID).
For multi-line entries, only the first line is prefixed which can give you a hard time when grepping logs.

Example

Rails.logger.info(<<~TEXT)
  Response from example.com:
  Status: 200
  Body: It works!
TEXT

With that, the following is written to your log file.

I, [2024-10-04T08:12:16.576463 #1917250]  INFO -- : [97e45eae-a220-412d-96ad-e9e148ead71d] Response from example.com:
Status: 200
Body: It works!

If you then run `grep...

Upgrading a Rails app to Cucumber 3

Upgrade gems

You need to update a lof gems. Make sure you don't have any version constraints in your Gemfile or your bundle update won't do anything!

Upgrade cucumber_priority:

bundle update cucumber_priority

Upgrade spreewald:

bundle update spreewald

Upgrade cucumber_factory:

bundle update cucumber_factory

Upgrade parallel_tests:

bundle update parallel_tests

Even on the latest version, parallel_tests will print some deprecation warnings due to using an older formatter A...

Rails: Using normalizes without copying code

Rails 7.1 added the normalizes method which can be used to normalize user input.

It lets you define the fields you want to normalize and how to normalize them. In the example below, the Movie#title attribute is stripped from leading and trailing whitespace automatically:

class Movie < ApplicationRecord
  normalizes :title, with: -> {  _1.strip }
end

Tip

Normalization lambdas are not called for nil values by default. To normalize nil values, pa...

Detect the current Rails environment from JavaScript or CSS

Detecting if a Javascript is running under Selenium WebDriver is super-painful. It's much easier to detect the current Rails environment instead.

You might be better of checking against the name of the current Rails environment. To do this, store the environment name in a data-environment of your <html>. E.g., in your application layout:

<html data-environment=<%= Rails.env %>>

Now you can say in a pi...

Rails cache connection settings

If you're using a Redis cache in Rails (e.g. :redis_cache_store), it's possible to configure additional parameters for your Redis connection.

Example config for Rails 7.2

config.cache_store = :redis_cache_store, {
  pool: { timeout: 0.5 },
  read_timeout: 0.2, # default 1 second
  write_timeout: 0.2, # default 1 second
  # Attempt two reconnects with some wait time in between
  reconnect_attempts: [1, 5], # default `1` attempt in Redis 5+
  url: REDIS_URL,
  error_handler: ->(method:, returning:, exception:) {
    Sentry.captur...

Searchkick: async reindexing fails for rails 7 with redis 4

After an upgrade to rails 7 I noticed that async reindexing jobs of Searchkick were failing for Model.reindex(mode: :async, wait: true):

/home/a_user/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/searchkick-5.3.1/lib/searchkick/relation_indexer.rb:142:in `block in batch_job': undefined method `call' for nil (NoMethodError)

    Searchkick.with_redis { |r| r.call("SADD", batches_key, [batch_id]) }
                                 ^^^^^
from /home/a_user/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/gems/searchkick-5.3.1/lib/searchkick.r...

Rails asset pipeline: Using ESNext without a transpiler

If your app does not need to support IE11, you can use most ES6 features without a build step. Just deliver your plain JavaScript without transpilation through Babel or TypeScript, and modern browsers will run them natively.

Features supported by all modern browsers include:

  • fat arrow functions (() => { expr })
  • let / const
  • class
  • async / await
  • Promises
  • Generators
  • Symbols
  • Rest arguments (...args)
  • Destructuring

You won't be able to use import and export, or use npm modules.

See this [ES6 compatibility mat...

How to organize monkey patches in Ruby on Rails projects

As your Rails project grows, you will accumulate a number of small patches. These will usually fix a bug in a gem, or add a method to core classes.

Instead of putting many files into config/initializers, I recommend to group them by gem in lib/ext:

lib/
  ext/
    factory_girl/
      mixin.rb
    carrierwave/
      change_storage.rb
      fix_cache_ids.rb
      sanitize_filename_characters.rb
    ruby/
      range/
        covers_range.rb
      array/
        dump_to_excel.rb
        xss_aware_join.rb
      enumerable/
    ...

Upgrade Rails: Awareness list

Disclaimer

This card is a collection of guides and things to have in mind when upgrading to a specific version. It is not meant to be complete, so please feel free to contribute!

General workflows

Upgrade to Rails 7

Rails: Flagging all cookies as secure-only to pass a security audit

Why secure-only cookies used to be necessary

Cookies have an optional secure flag. It tells the browser to not send the cookie for a non-https request.

It used to be important to activate the secure flag even on sites that automatically redirect users from http:// to https://. The reason was that most users will only enter a scheme-less domain like makandra.de into their location bar, which will default to `http://m...

Rails: Using require and permit for attributes

Raising errors for required and permitted attributes makes it easier to find errors in your application during development and in tests. Consider this approach if you want to strengthen the params handling in your application.

Example

# config/application.rb

config.action_controller.action_on_unpermitted_parameters = :raise
def user_params
  params.require(:user).permit(:full_name)
end

Effects

  • This raises an error `Ac...

Rails developers: Have better context in Git diffs

Git diffs show the surrounding contexts for diff hunks. It does so by applying regular expressions to find the beginning of a context. When it comes to Ruby, however, it will not find method heads and travel up to the class definition:

@@ -24,7 +24,7 @@ class TicketPdf # <=== Actually expected here: the method definition
     ApplicationController.render(
       "tickets/index.html.haml",
       layout: "tickets",
-      assigns: { tickets: tickets }
+      assigns: { tickets: tickets, event_name: event_name }
     )
   end
 end
```...

How to keep using secrets.yml after upgrading to Rails 7.2

Rails 5.2 soft-deprecated the storage of secrets in secrets.yml in favor of a new thing, credentials.yml.enc. Rails 7.1 deprecated secrets and Rails 7.2 finally removed it.

In our permissions model, it does not matter much whether secrets or credentials are used. While we'll use credentials in new applications (for conformity), for existing applications it may be appropriate to keep using secrets.yml.

Restoring secrets in Rails 7.2+

Restoring `Rails.applic...

Taming icon fonts for use in Rails views

Icon fonts like Font Awesome are infinitely scalable, look great on high-DPI displays and will give your app a modern look.

However, icon fonts can be very awkward to use compared to raster icons. Elements are given icons by giving them a special class like icon-plus or icon-home:

<span class="icon-plus">Create</span>

The icon font's stylesheet will then recognize this class and insert the icon as the element's :before style.

In practic...