Maintaining custom application tasks in Rails
Here are some hints on best practices to maintain your tasks in larger projects.
Rake Tasks vs. Scripts
- The Rails default is using rake tasks for your application tasks. These live in
lib/tasks/*
. - In case you want to avoid rake for your tasks and just use plain ruby scripts, consider
lib/scripts/*
as folder.
Keeping tasks slim
For readability and testing it's easier to keep your tasks slim. We suggest to use folders inside the tasks
or scripts
folder.
Example for a task:
The slim task lib/tasks/gitlab.rb
:
Rails: Accessing strong parameters
Rails wraps your parameters into an interface called StrongParameters. In most cases, your form submits your data in a nested structure which goes hand in hand with the strong parameters interface.
Example:
curl -X POST -d "user[name]=bob" https://example.com/users
class UsersController
def create
User.create!(params.expect(user: [:name])) # Or User.create!(params.require(:user).permit(:name))
end
end
This works well most of the time...
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:
- Enable caching for every test (not covered by this card and straightforward)
- Enable caching for individual test
Enable caching for individual test (file cache)
1. Leave the defau...
How to set up database_cleaner for Rails with Cucumber and RSpec
Add gem 'database_cleaner'
to your Gemfile. Then:
Cucumber & Rails 3+
# features/support/database_cleaner.rb
DatabaseCleaner.clean_with(:deletion) # clean once, now
DatabaseCleaner.strategy = :transaction
Cucumber::Rails::Database.javascript_strategy = :deletion
Cucumber & Rails 2
The latest available cucumber-rails
for Rails 2 automatically uses database_cleaner
when cucumber/rails/active_record
is required -- but only if transactional fixtures are off. To have database_cleaner
work correctly:
- Add the ...
How to use Simplecov to find untested code in a Rails project with RSpec and Cucumber
Simplecov is a code coverage tool. This helps you to find out which parts of your application are not tested.
Integrating this in a rails project with rspec, cucumber and parallel_tests is easy.
-
Add it to your Gemfile and bundle
group :test do gem 'simplecov', require: false end
-
Add a
.simplecov
file in your project root:SimpleCov.start 'rails' do # any custom configs like groups and filters can be here at a central place enable_cov...
How to define a table name prefix for all Rails models in a given namespace
ActiveRecord computes table names of model classes, and results are usually just like you'd expect.
Adding a prefix for all classes in a namespace can feel a odd or broken, but does not have to be. It's actually very easy when done right.
Summary (tl;dr)
Here's the short version: Define a table_name_prefix
method in the namespace module, and do not define any table_name_prefix
in ActiveRecord classes inside of it. If this sounds familiar, we have [a card about using it already](https://makandracards.com/makandra/47198-rails-namespac...
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: Overriding view templates under certain conditions only
Rails offers a way to prepend (or append) view paths for the current request. This way, you can make the application use different view templates for just that request.
Example
A use case of this is a different set of view templates that should be used under certain circumstances:
class UsersController < ApplicationController
before_action :prepare_views
def index
# ...
end
private
def prepare_views
if <condition>
prepend_view_path Rails.root.join('app', 'views', 'special')
end
end
...
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...
routing-filter is broken with Rails 7.1
If you are using the routing-filter gem in your Rails 7.1 app for managing URL segments for locales or suffixes, you will notice that the filters do no longer apply, routes are broken and the necessary parameters are no longer extracted. That is because routing-filter patches Rails' find_routes
-method to get the current path and apply its defined filters on it. These filters then modify the params that are handed over to your controller action. This way you receive a locale
parameter from a ...
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: Composing an ETag from multiple records
Rails offers the fresh_when
method to automatically compute an ETag from the given record, array of records or scope of records:
class UsersController < ApplicationController
def show
@user = User.find(params[:id])
fresh_when @user
end
def index
@users = User.all.to_a
fresh_when @users
end
end
When your view also displays other records (typically associations), those other records should be included in the ETag
. You can do so by passing an array of ETaggable objects to fresh_when
.
...
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 ...
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 normalizenil
values, pa...
Rails: Do not load frameworks you don't need
Rails is split into a large number of (sub-) frameworks.
The most important and central of those are
- activesupport (extends the Ruby standard library)
- activerecord / activemodel (ORM for Rails)
- actionview / actionpack (controller / views)
- actionmailer (sends mails)
However, there are also some more situational frameworks included, such as
- actioncable (real time communications using websockets)
- actionmailbox (receives mails)
- actiontext (support for WYSIWYG text editor)
- activejob (background jobs)
- activestorage (file uplo...
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...
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: 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 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...
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:
...
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
- Don't use log level :debug in your production environments
- [Rails 7.1: Take care of...
Rails: Fixing the memory leak / performance issues in prepend_view_path
Recently we detected a memory leak in one of our applications. Hunting it down, we found that the memory leak was located in Rails' #prepend_view_path
. It occurs when the instance method prepend_view_path
is called in each request, which is a common thing in a multi-tenant application.
On top of leaking memory, it also causes a performance hit, since templates rendered using the prepended view path will not be cached and compiled anew on each request.
This is not a new memory leak. It was [first reported in in 2014](https://github.com/...