Use find_in_batches or find_each to deal with many records efficiently
Occasionally you need to do something directly on the server -- like having all records recalculate something that cannot be done in a migration because it takes a long time.
Let's say you do something like this:
Project.all.each(&:recalculate_statistics!)
Even though you may have been successful with this on your development machine or the staging server, keep in mind that production machines often hold a lot more records. Using all
may just work, even with lots of records, but when you iterate over such records and fetch a...
RSpec: Increase readability with super_diff
When handling nested hashes the RSpec output is often hard to read. Here the gem super_diff
could help.
Add super_diff to your project
- Add
super_diff
to your Gemfile:
gem 'super_diff'
- Require it in your
spec_helper.rb
require 'super_diff/rspec' # For Rails applications you can replace this with 'super_diff/rspec-rails'
-
Customize colors in
spec/support/super_diff.rb
SuperDiff.configure do |config|
config.ac...
Tod: A Gem for handling daytime without a date
Tod is a gem for working with daytimes. That's a tuple of (hour, minute second)
without a day, month or year.
Another additional gem?
Thus SQL has a time
datatype for storing time of day in the format hh:mm:ss
, neither Ruby nor Rails themselves offer an elegant way to deal with day times.
Time
and DateTime
both handle daytime values AND calendar date, using them to only store the time of day will end in inconsistent and thus confusing data, e. g. Time.new
will initialize with the current Time in your Timezone, `DateTime.n...
What edge_rider offers you
edge_rider is Power tools for ActiveRecord relations (scopes). Please note that some of the functions edge_rider provides have native implementations in newer rails versions.
Useful in applications
Relation#traverse_association(*names)
Edge Rider gives your relations a method #traverse_association
which returns a new relation by "pivoting" around a named association. You can traverse multiple associations in a single call. E.g. to turn a relation of posts into a relation of all posts o...
Self-expiring URLs with Apache
When delivering non-public uploaded files (images, documents etc), one has to decide whether and how to do authorization. The usual approaches are:
- Using
send_file
with a regular controller. This is secure, but potentially slow, especially for large collections of images. - Using unguessable URLs. This is fast (because Apache can deliver assets without going through Rails), but less secure.
When going with the "unguessable URL" approach, it is possible to somewhat increase security by using expiring URLs. The idea is to encode the expi...
Carrierwave: How to attach files in tests
Attaching files to a field that is handled by Carrierwave uploaders (or maybe any other attachment solution for Rails) in tests allows different approaches. Here is a short summary of the most common methods.
You might also be interested in this card if you see the following error in your test environment:
CarrierWave::FormNotMultipart:
You tried to assign a String or a Pathname to an uploader, for security reasons, this is not allowed.
If this is a file upload, please check that your upload form is multipart encoded.
Factor...
How to list updateable dependencies with Bundler and Yarn
Bundler
bundle outdated [--filter-major|--filter-minor|--filter-patch]
Example output for bundle outdated --filter-major
Other examples
A useful flag is --strict
as it will only list versions that are allowed by your Gemfile requirements (e.g. does not show rails update to 6 if your Gemfile has the line gem 'rails', '~>5.2'
).
I also experienced that doing upgrades per group (test, development) are easier to do. Thus --groups
might also be helpful.
$ bundle...
makandra cards: A knowledge base on web development, RoR, and DevOps
What is makandra cards?
We are makandra, a team of 60 web developers, DevOps and UI/UX experts from Augsburg, Germany. We have firmly anchored the sharing of knowledge and continuous learning in our company culture. Our makandra cards are our internal best practices and tips for our daily work. They are read worldwide by developers looking for help and tips on web development with Ruby on Rails and DevOps.
15 years ago – in 2009 – we wrote our first card. Since then, over 6000 cards have been created, not o...
Differences between transactions and locking
Web applications can be used by multiple users at the same time. A typical application server like Passenger has multiple worker processes for a single app. In a distributed deployment setup like we use at makandra you will even have multiple application servers, each with their own worker pool.
This means that your code needs to deal with concurrent data access. The two main tools we use to cope with concurrency are database transactions and distributed locks. These two are not interchangeable. You ca...
RSpec: How to write isolated specs with cookies
Background
Rails offers several methods to manage three types of different cookies along with a session storage for cookies. These are normal, signed and encrypted cookies.
By following the happy path of testing a web application, that is only the main use-case is tested as a integration test and the rest as isolated (more unit ...
Escape a string for transportation in a URL
To safely transport an arbitrary string within a URL, you need to percent-encode characters that have a particular meaning in URLs, like &
or =
.
If you are using Rails URL helpers like movies_path(:query => ARBITRARY_STRING_HERE)
, Rails will take care of the encoding for you. If you are building URLs manually, you need to follow this guide.
Ruby
In Ruby, use CGI.escape
:
# ✅ good
CGI.escape('foo=foo&bar=bar')
=> "foo%3Dfoo%26bar%3Dbar"
Do not ever use `URI.en...
Capistrano task to edit staging / production credentials
When using Rails credentials, you will edit the encrypted credentials for staging or production environments from time to time. To do that you need the secret key which should only live on the servers.
Do not download these key files to your local dev environment. They are sensitive and must not be stored on your machine.
Instead, put the attached capistrano task into lib/capistrano/tasks/
of your application. It expects environment specific keys to live in :shared_path/config/credentials/:stage.key
. If you have a single master.key...
Handy: A regex that validates all valid email addresses (give or take) - Axon Flux // A Ruby on Rails Blog
/^([\w!#$%&'*+-/=?^`{|}~]+.)*[\w!#$%&'*+-/=?^`{|}~]+@((((([a-z0-9]{1}[a-z0-9-]{0,62}[a-z0-9]{1})|[a-z]).)+[a-z]{2,6})|(\d{1,3}.){3}\d{1,3}(:\d{1,5})?)$/i
Devise: Invalidating all sessions for a user
Background information about session storage in Rails
Rails has a default mechanism to store the session in the CookieStore
. This is a cookie which holds the entire user session hash in the browser. This cookie is serialized, encoded with base64, and signed.
How Devise handles authentication
Devise uses this CookieStore
. To track a users session, a salt is stored in the session ...
Enabling YJIT
YJIT is Ruby's default just-in-time compiler. It is considered production-ready since Ruby 3.2 (source).
To activate YJIT you need two steps:
- Your
ruby
binary needs to be compiled with YJIT support. - You need to enable YJIT.
Getting a Ruby with YJIT support
We usually install Ruby with tools like rbenv
or asdf
. This compiles the ruby
binary from the source code. Support for YJIT
will be automatically added during this compilation...
Best practices: Large data migrations from legacy systems
Migrating data from a legacy into a new system can be a surprisingly large undertaking. We have done this a few times. While there are significant differences from project to project, we do have a list of general suggestions.
Before you start, talk to someone who has done it before, and read the following hints:
Understand the old system
Before any technical considerations, you need to understand the old system as best as possible. If feasible, do not only look at its API, or database, or frontend, but let a user of the old system sho...
Configuring Webpacker deployments with Capistrano
When deploying a Rails application that is using Webpacker and Capistrano, there are a few configuration tweaks that optimize the experience.
Using capistrano-rails
capistrano-rails is a Gem that adds Rails specifics to Capistrano, i.e. support for Bundler, assets, and migrations. While it is designed for Asset Pipeline (Sprockets) assets, it can easily be configured for Webpacker. This brings these features to the Webpacker world:
- Automatic removal of expired assets
- Manifest backups
Ruby: How to determine the absolute path relative to a file
If you want to get the path of a file relative to another, you can use the expand_path
method with either the constant __FILE__
or the method __dir__
. Read this card for more information about __FILE__
and __dir__
.
Example
Structure:
.
├── bin
│ ├── format_changelog
├── CHANGELOG.md
bin/format_changelog
:
#!/usr/bin/env ruby
changelog_path = ? # How to get the path to ../CHANGELOG.md independent of the working dir of the caller
changelog = File.read(changelog_path)
# ... further actions...
Don't use log level :debug in your production environments
Catch phrase
You don't want sensitive user data in your logs.
Background
Rails per default filters sensitive data like passwords and tokens and writes [FILTERED]
to the logs. The code which is responsible for enabling that usually lives in filter_parameter_logging.rb
(Rails.application.config.filter_parameters
). Here is an example of a filtered log entry:
Unfiltered:
`User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."token" = $1 LIMIT $2 [["token", "secret-token"], ["LIMIT", 1]]`
After the filter is appl...
Use Time.current / Date.current / DateTime.current on projects that have a time zone
Basically, you now need to know if your project uses a "real" time zone or :local
, and if config.active_record.time_zone_aware_attributes
is set to false
or not.
-
With time zones configured, always use
.current
forTime
,Date
, andDateTime
.ActiveRecord attributes will be time-zoned, and
.current
values will be converted properly when written to the database.
Do not useTime.now
and friends. Timezone-less objects will not be converted properly when written to the database. -
With no/local time zone use
Time.now
, `...
Livereload + esbuild
Getting CSS (and JS) live reloading to work in a esbuild / Rails project is a bit of a hassle, but the following seems to work decently well.
We assume that you already use a standard "esbuild in Rails" setup, and have an esbuild watcher running that picks up your source code in app/assets
and compiles to public/assets
; if not change the paths below accordingly.
Basic idea
We will
- use the
guard-livereload
gem as the livereload server (which send updates to the browser), - use the
livereload-js
npm package in the browser to con...
RSpec: Where to put custom matchers and other support code
Custom matchers are a useful RSpec feature which you can use to DRY up repetitive expectations in your specs. Unfortunately the default directory structure generated by rspec-rails
has no obvious place to put custom matchers or other support code.
I recommend storing them like this:
spec/support/database_cleaner.rb
spec/support/devise.rb
spec/support/factory_bot.rb
spec/support/vcr.rb
spec/support/matchers/be_allowed_access.rb
s...
How to discard ActiveRecord's association cache
You know that ActiveRecord caches associations so they are not loaded twice for the same object. You also know that you can reload
an association to make Rails load its data from the database again.
user.posts.reload
# discards cache and reloads and returns user.posts right away
# => [...]
If you want to discard the cache but not query the database (only the next time the association is accessed), you can use reset
:
user.posts.reset
# discards cache, but does not load anything yet
user.posts
# SQL query happens to ...
Ruby: Referencing global variables with the built-in English library
tl;dr
Don't forget
require 'English'
if you use a named global such as$LAST_MATCH_INFO
. Otherwise this could result in an annoying bug.
With Ruby's build-in library English you can reference global variables with an english name. This makes you code easier to read and is also suggested by Rubocop's Style/GlobalVars cop.
Example before:
if 'foo' =~ /foo/
puts $~[1] # => foo
end
Example af...