Posted over 4 years ago. Visible to the public.

Decorating ActiveRecord

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

Copy
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 Archive . Given this decorator:

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

What happens in this view?

Copy
<%= 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):

Copy
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:

Copy
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:

Copy
<%= 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:

Copy
class Profile < SimpleDelegator extend ActiveModel::Naming def to_model self end end

Owner of this card:

Avatar
Alexander M
Last edit:
over 4 years ago
by Alexander M
Tags:
Software-Architecture
Posted by Alexander M to Ruby and RoR knowledge base
This website uses short-lived cookies to improve usability.
Accept or learn more