As I’ve previously mentioned, I’m building a brand-new business from scratch on top of Rails. Reversing key architectural decisions is like getting a bad tattoo lasered off—it takes time, it’s expensive, and your bad decision will never truly fade into oblivion. With that in mind, I’ve been overcautious in doing my research when putting new architectural patterns into place, which led me to a cool solve while researching solutions to my most recent problem.

My app has a common concept of Users which can take on many different Roles. Each user can have a profile, but the information contained in the profile differs based on the user’s role. In object-oriented programming, the design is clear: we would have a BaseProfile with common fields and methods and subclasses for our specific types. Unfortunately, there’s no perfect way to translate this schema onto a database. As of now, Rails has three main ways to solve this problem:

  1. Single-table inheritance (STI)—all fields for all profile types are stored in one database table. If a field is not needed for a specific profile type, its value will be nil. This can be an issue if each type of profile has lots of different fields, because the table will be sparsely filled and a lot of space (and therefore, efficiency) is wasted.
  2. Multi-table inheritance (MTI)—in this strategy, each type of profile is stored in its own table. The problem here is that I wanted to connect a User with their Profile directly so I could do things like user.profile. To set up this type of relationship, I would have to include an association for each subclass to connect the User model with each Profile table.
  3. Polymorphism—the most “Rails-y” way of solving the problem. Essentially, you have one table, but that table has a type field that tells the record how to act.

All of these solutions have benefits and drawbacks, all of which have already been covered by folks much smarter than me. But I do have one advantage—I’m starting from scratch, so I can use the latest tech.

And lo and behold, just last month, DHH (the creator and BDFL of Rails) put up a PR for a new solution called “delegated type”. It’s definitely worth clicking through to read DHH’s description of the problem he’s trying to solve. I won’t go into them here, but I did want to outline some of the problems I had getting this to work as there’s not a lot of documentation out there.

Note that delegated types are coming in Rails 6.1. As DHH, the PR is just syntactic sugar on top of polymorphic associations, but I’m too lazy (okay, fine, I’m not smart enough) to do this on my own, so I just pointed Rails at master in my Gemfile.

Delegated types with simple_form and form_for

Profiles are only useful if you can fill them out! And I picked Rails because it’s dead simple to create a model and generate a corresponding form. This is made slightly more difficult by our use of delegated types. Rails includes a method called accepts_nested_attributes_for that allows a model to accept attributes on behalf of another model type that it is associated with. The name of this helper method led me to believe that Profile should accepts_nested_attributes_for SubProfile. But users are never filling out forms for BaseProfile. They only care about the specific type of Profile for their User type. Therefore, counterintuitively, SubProfile accepts_nested_attributes_for Profile.

module Profileable
  extend ActiveSupport::Concern

  included do
    has_one :profile, as: :profileable, touch: true, dependent: :destroy
    accepts_nested_attributes_for :profile
  end
end


class Profile < ApplicationRecord
  belongs_to :user
  delegated_type :profileable, types: %w[ author editor reader]
end

class Author < ApplicationRecord
  include Profileable
end

Integrating Pundit is dead simple

Since delegated types actually gives a defined superclass backed by its own table, we can tie permissions and associations to the superclass and the rest of the subclasses will inherit the appropriate information. You can see this in the example above, Profile belongs_to :user, not Author.

Since in our case, we want to treat all user profiles the same in terms of visibility and permissions, we don’t need to write Pundit policies for each type of Profile. Instead, we can write a single ProfilePolicy and tell our different types of profiles to use that as their policy:

class Author < ApplicationRecord
  include Profileable

  def policy_class
    ProfilePolicy
  end
end

The documentation is currently wrong for creating new delegated types

DHH says that in his example, a new record can be created via the following:

Entry.create! message: Comment.new(content: "Hello!"), creator: Current.user

Trying this format with my own implementation-specific code yields an error similar to:

ActiveModel::UnknownAttributeError (unknown attribute 'message' for Profile.)

But further down the thread, he says the correct incantation is:

Entry.create! entryable: Message.new(subject: "hello!"), creator: Current.user

And indeed, using entryable instead worked for me. This issue has already been fixed in the code documentation, robbing me of a golden opportunity to contribute to Rails core and, by extension, legitimizing my rockstar programming status.

Let me know if you have further questions about delegated types! I’m sure I missed a few gotchas that I’ve since forgotten and will update this blog post as they trickle back in.