Rails 4 in production: strong parameters, Turbolinks, and Russian-doll caching
Rails 4.0 has just shipped. Here is a thorough, from-the-trenches look at what we are adopting on real projects — strong parameters, the Turbolinks trade-off, Russian-doll caching, first-class Postgres, and the upgrade gotchas.
Rails 4.0 landed at the end of June, and we have already moved a couple of projects onto it. It is a smaller release than 3.0 was — there is no single earth-moving change like the 2.x-to-3.0 merge with Merb — but a surprising number of the new defaults touch how you write everyday code, and a few of them will quietly change the shape of your application. Here is the pragmatic view from the trenches: what we are adopting eagerly, what we are switching off, and the upgrade gotchas that cost us an afternoon each.
Strong Parameters: mass-assignment protection grows up
The headline change to day-to-day code is that mass-assignment protection has
moved out of the model and into the controller. The old attr_accessible and
attr_protected are gone from core — they live on in the protected_attributes
gem if you genuinely need them during a migration — and in their place you
whitelist parameters where the request actually arrives:
class PeopleController < ApplicationController
def create
@person = Person.create(person_params)
# ...
end
def update
@person = Person.find(params[:id])
@person.update(person_params) # note: `update`, not `update_attributes`
end
private
def person_params
params.require(:person).permit(:name, :email, :bio, role_ids: [])
end
end
The why matters more than the mechanics. Under the old scheme, what a user was
allowed to set was a property of the model, declared once, far from any
particular request. The moment you needed an admin to set fields a normal user
could not, you reached for attr_accessible :role, as: :admin and a :as role
threaded through every save — clumsy, easy to get wrong, and invisible at the
call site. Strong Parameters flips the responsibility to the controller, which is
the layer that actually knows the context: who is making this request and
what they are allowed to change.
A few things worth knowing once you start using it in anger:
-
requireasserts the key is present and not empty, raisingActionController::ParameterMissing(a 400, not a 500) if the wholepersonhash is missing.permitwhitelists the scalar attributes inside it. -
Nested and collection attributes need explicit shapes. A
has_manythroughaccepts_nested_attributes_foris permitted with a nested hash, and array values need the trailing[]:params.require(:project).permit( :name, tag_ids: [], tasks_attributes: [:id, :title, :done, :_destroy] ) -
Anything not permitted is stripped, and in development you get a log line telling you which keys were filtered out — read it, because a silently dropped attribute is a confusing bug otherwise.
-
params.require(:person).permit!permits everything. It exists; treat it as a code smell, because it is the exact hole Strong Parameters was built to close.
This reads better than scattering attr_accessible across models, the whitelist
sits next to the action that understands the context, and the failure mode (a 400
on a missing param) is far friendlier than a silent mass-assignment. We like it
unreservedly.
Turbolinks: fast by default, but know the trade-off
Turbolinks is the most controversial default in the release, and the one we most
often turn off. The idea is genuinely clever: instead of a full page load on
every link click, Turbolinks intercepts the click, fetches the new page over XHR,
swaps the <body> and the <title>, and keeps the same JavaScript runtime
alive across the navigation. Because the browser never reparses your CSS and JS,
content-heavy, server-rendered pages feel almost instant. On the right app it is
a real, free speed-up.
The catch is the part people skip: “keeps the same JavaScript runtime alive”
means the page never actually reloads, so the events you have always relied on
stop firing the way you expect. The classic break is $(document).ready — it
fires once, on the first real load, and never again as you Turbolink between
pages. Any code that binds handlers on ready silently stops working after the
first visit, and then mysteriously works again after a hard refresh, which is a
maddening bug to chase.
You have two honest options. Embrace it — bind with event delegation so handlers
survive body swaps, and listen for Turbolinks’ own lifecycle events instead of
ready:
// instead of $(document).ready(...)
$(document).on("page:load", function () {
initSomeWidget();
});
// delegation survives body replacement:
$(document).on("click", ".js-toggle", function () { /* ... */ });
Or, on a JS-heavy app where you are constantly fighting the lifecycle, drop it:
# Gemfile — remove turbolinks entirely, or keep it and opt pages out
# per-link with data-no-turbolink on the anchor or a container
gem "turbolinks"
There is also a sharp edge around memory: because the runtime persists, handlers
bound to document accumulate on every visit unless you are careful, and a leak
that a full page load would have swept away now lingers for the life of the tab.
Our rule of thumb: keep Turbolinks for mostly-static, server-rendered pages where it shines, and turn it off for anything with a lot of bespoke jQuery or a complex client-side lifecycle. The point is to make Turbolinks a deliberate decision rather than a default that ambushes you in production a week after launch.
Russian-doll caching: the feature we are happiest about
Nested fragment caching with automatic key invalidation — “Russian-doll caching”,
powered by the new cache_digests — is the change we have got the most mileage
from. The pattern is to cache a collection, cache each item inside it, and let
the keys nest so that when one item changes, only its fragment and its ancestors
expire, while its siblings stay warm:
<% cache @project do %>
<h1><%= @project.name %></h1>
<% @project.todos.each do |todo| %>
<% cache todo do %>
<%= render todo %>
<% end %>
<% end %>
<% end %>
Two pieces of machinery make this work, and both are worth understanding.
First, the cache key includes a digest of the template. The framework hashes the contents of the view (and its dependencies — the partials it renders) and bakes that hash into the cache key. The payoff is enormous: when you edit a partial, its digest changes, so the old fragment is simply never looked up again. This kills the single most annoying caching bug — stale views served after a deploy because the markup changed but the cache key did not. You no longer sweep caches by hand on deploy; obsolete fragments just age out.
Second, touch: true propagates freshness up the tree. For the outer fragment
to expire when an inner record changes, the parent’s updated_at has to move when
the child does. You wire that with belongs_to ... touch: true:
class Todo < ActiveRecord::Base
belongs_to :project, touch: true # saving a todo bumps project.updated_at
end
Now updating a single todo touches its project, the project’s cache key changes,
its fragment expires and re-renders — but every other todo fragment is read
straight from the cache. With a memory-backed store (Memcached or Redis via
dalli) in front of a content-heavy page, the speed-up is dramatic and, unlike
Turbolinks, has no behavioural downside to weigh.
PostgreSQL gets first-class treatment
Rails 4 ships native support for a swathe of PostgreSQL types that previously
needed custom serialization or a gem: hstore, arrays, inet/cidr, uuid,
and json. For the projects where we already lean on Postgres, this deletes a
pile of glue code.
class AddPreferencesToUsers < ActiveRecord::Migration
def change
add_column :users, :preferences, :hstore
add_column :users, :tag_list, :string, array: true, default: []
add_index :users, :preferences, using: :gin
end
end
user.preferences = { "theme" => "dark", "locale" => "en" }
user.tag_list = ["ruby", "rails"]
user.save
The hstore key-value column with a GIN index is a pragmatic middle ground
between rigid columns and reaching for a document database — schemaless-ish fields
that still live in a real relational store you can join and transact against.
Array columns remove a swathe of trivial join tables. This is the start of a long
trend of Postgres absorbing features people used to leave Rails for, and Rails 4
is where it becomes comfortable.
The upgrade gotchas
A few things bit us moving real apps across, none fatal but all worth knowing:
- Threadsafe is the default now. Rails 4 runs in threadsafe mode out of the
box. If you have non-thread-safe code or class-level mutable state, this is
where it surfaces — audit your initializers and any
@@class_variables. - Dynamic finders are gone.
find_all_by_x,find_last_by_x, and friends are removed (the singlefind_by_xsurvives, plus the newfind_by). The replacement is the relation API:where(x: ...),find_by(x: ...). - Default scopes and
scopeneed a callable.scope :published, where(...)now wants a lambda:scope :published, -> { where(published: true) }. update_attributesbecomesupdate, andupdate_attribute(singular, skips validation) is still a trap — preferupdateorupdate_columnand know which one skips what.- Ruby 1.9.3 is the floor, and 2.0 is where you want to be for the GC and keyword-argument improvements.
- Concerns get a home.
app/models/concernsandapp/controllers/concernsare first-class, andActiveSupport::Concernis the idiomatic way to share behaviour — a nudge toward composition over fat base classes.
Verdict
Rails 4 is an easy upgrade to recommend. Strong Parameters and Russian-doll
caching alone repay the effort: the first makes your security model legible and
local, the second makes view performance almost free once you have wired up
touch:. First-class Postgres types are a quiet luxury that deletes code. The one
thing not to sleepwalk through is Turbolinks — it is a genuine win on the right
pages and a genuine headache on the wrong ones, so make a deliberate decision per
app rather than letting the default surprise you in production. Do that, audit
your initializers for the threadsafe switch, and the move is smooth.