Decorating ActiveRecord

Posted Almost 7 years ago. Visible to the public.

It’s common to use the decorator/presenter pattern to wrap ActiveRecord objects and add view-specific logic. For example:

class Profile < SimpleDelegator
  def title
    "#{first_name} #{last_name}'s profile"
  end
end

# controller
class ProfilesController < ApplicationController
  def show
    @profile = Profile.new(current_user)
  end
end

# some view
<h1><%= profile.title %></h1>

Things fall apart

Simple enough. Now let’s look at some examples that leverage to_partial_path Show archive.org snapshot . Given this decorator:

class Profile < SimpleDelegator
  def to_partial_path
    "profiles/profile"
  end
end

What happens in this view?

<%= render profile %>

t will not render the profiles/profile partial but will instead render the users/user partial. What happened? The Rails rendering code has a conditional that looks like this (simplified):

if object.responds_to?(:to_model)
  path = object.to_model.to_partial_path
else
  path = object.to_partial_path
end

For most objects where you define to_partial_path, the rendering code will take that second branch and just call to_partial_path on your object. However, decorated ActiveRecord objects do respond to to_model so the rendering will take that first branch. What happens when you call to_model on a decorator? It delegates to the wrapped user’s implementation which returns itself. This means calling to_model on the decorator returns the unwrapped user. This all means that User#to_partial_path gets invoke rather than Profile#to_partial_path. The solution is to define to_model on the decorator:

class Profile < SimpleDelegator
  def to_model
    self
  end

  # other things
end

A matter of form

So all you need to do is define to_model on your decorator? Not so fast! Rails has some other surprises up its sleeve. How about something like this:

<%= form_for Profile.new(User.new) do |f| %>
<% end %>

You might expect this form to POST to /profiles but instead it will submit to /users. Isn’t the path based on the class name? Apparently not. Instead the URL is built based on attributes of your object’s model_name. Since model_name is delegated to the underlying user, the form submits to /users. This can be fixed by extending ActiveModel::Naming (which defines model_name). Now it’s generating paths for “profiles” instead of “users”.

Playing nicely with ActiveRecord and ActiveModel

If you’re decorating an ActiveRecord or ActiveModel object in Rails, you probably want to define the following to ensure the decorator works the way you expect instead of silently delegating to the underlying object:

class Profile < SimpleDelegator
  extend ActiveModel::Naming

  def to_model
    self
  end
end
Alexander M
Last edit
Almost 7 years ago
Alexander M
Posted by Alexander M to Ruby and RoR knowledge base (2017-04-27 14:52)