Skip to content
← All posts
8 min read Dawid Skłodowski

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:

  • require asserts the key is present and not empty, raising ActionController::ParameterMissing (a 400, not a 500) if the whole person hash is missing. permit whitelists the scalar attributes inside it.

  • Nested and collection attributes need explicit shapes. A has_many through accepts_nested_attributes_for is 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 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 single find_by_x survives, plus the new find_by). The replacement is the relation API: where(x: ...), find_by(x: ...).
  • Default scopes and scope need a callable. scope :published, where(...) now wants a lambda: scope :published, -> { where(published: true) }.
  • update_attributes becomes update, and update_attribute (singular, skips validation) is still a trap — prefer update or update_column and 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/concerns and app/controllers/concerns are first-class, and ActiveSupport::Concern is 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.