Intro

Modeling data can be the most challenging part of creating a new app. In fact, it’s been suggested that it’s totally acceptable to -not- fail fast when scoping your schema. Getting your data modeled correctly from your Initial Commit can save you hours of refactoring.

I was recently prototyping a new greenfield application for work, and as I mapped out my models and how they’d be associated, I started to experience some friction.

Just when I’d found my preferred way to use ActiveRecord queries that read like natural language, I’d throw off the conventional readability of my schema.rb.

Going in and fixing the schema would undo some of my intuitive AR queries, or require some convoluted controller actions.

I came to the conclusion that all of this back-and-forth was due to Object-Relational Impedence Mismatch. Don’t worry if you’ve never heard of it, we’ll use an example app to demonstrate what it is and how Rails delegated_types offers a solution.

Example App Domain

Let’s imagine we’re creating an app for a fancy new generative coding assistant called….

….drumroll….

  .o    .oooo.                              o8o 
o888   d8P'`Y8b                             `"' 
 888  888    888 oooo    ooo      .oooo.   oooo 
 888  888    888  `88b..8P'      `P  )88b  `888 
 888  888    888    Y888'         .oP"888   888 
 888  `88b  d88'  .o8"'88b   .o. d8(  888   888 
o888o  `Y8bd8P'  o88'   888o Y8P `Y888""8o o888o

10x.ai !! Did we just nail our seed funding pitch in a blog post? Just by shoehorning “AI” somewhere into our business name? I think so!

Don’t worry about the AI aspect of it, sadly we’ll just be focusing on the subscriptions and payments.

We’ll support a monthly, a yearly, and a lifetime subscription.

Let’s also allow for users to have stripe payments or paypal payments.

Thinking About the Data

Starting with subscriptions, how could we handle storing these in the database and using them?

class-table inheritance

We could create a parent class and have a separate table for each sub-class. That might require us to duplicate quite a handful of attributes. Perhaps each subscription would have an is_active attribute. We’d need that column on each of our tables, and any other shared attributes would also need to be repeated on each table. Writing clean and optimized queries could become difficult with all that repitition.

single-table inheritance (sti)

We could also try to create one table of Subscriptions and maybe even use an enum to determine the type of subscription. This definitely gives us fewer tables to track down, but we have to persist any attribute of any subscription to this single table. This could lead to our Subscriptions having a load of attributes that aren’t relevant to a specific instance, e.g, a lifetime subscription would have no need for the end_date column, and might even cause some bugs when its value is nil.

Let’s digest these two approaches for a moment. We’ll come back.

What IS Object-Relational Impedence Mismatch?

from 10,000 feet -

Object-relational impedence mismatch (wikipedia) describes how objects you create in your code don’t fit easily into database rows and columns.

from a bit closer -

Using inheritance and building classes in OOP languages allows us to perform operations in a way that’s fundamentally disparate from how we want to persist information into a database. Imagine a UML diagram vs. an Excel spreadsheet - a single table from a UML diagram can look very similar to a single spreadsheet, but adding dynamic attributes and using their properties with other tables is not as simple as column/row = value.

up close and personal, back to our example -

Using only the limited scope of our two humble tables, we can get into a very inelegant predicament in no time. Suppose we want to get the payment information for a single YearlySubscription. How would we know whether a customer used a stripe or paypal payment?

I sure would hate to check for .stripe_payment.present? or .paypal_payment.present? every time we wanted to view billing history.

There is a better way!

Implementing delegated_types

ActiveRecord’s Delegated Types offers a way to manage shared behavior from a parent class like one would with OOP inheritance, while still allowing for unique “subclasses” in the form of separate models. The caveat here is that this approach will require a bit more hands-on manipulation of data.

Since delegated types help manage shared behavior, they fit nicely into the pattern of a Rails concern. Let’s delegated_type-ify our subscription.

the Subscription “superclass” from which the delegated types are derived-
# app/models/subscription.rb

class Subscription < ApplicationRecord
    has_many :payments # Also a delegated_type!
    delegated_type :subscribable, types: [ "MonthlySubscription",
                                           "YearlySubscription",
                                           "LifetimeSubscription"
                                           ]
    # And some optional syntactic sugar
    accepts_nested_attributes_for :subscribable

    def discounted_cost
        subscribable.discount_amount * cost
    end
end
the Subscribable concern
# app/models/concerns/subscribable.rb

module Subscribable
    extend ActiveSupport::Concern

    included do
        has_one :subscription, as: :subscribable, touch: true
    end
end
and an example of one of the Sub…Subscriptions
# app/models/monthly_subscription.rb

class MonthlySubscription < ApplicationRecord
    include Subscribable
    has_many :payments, through: :subscription

    def discount_amount
        .1
    end
end
creating a subscription

And finally to create a subscription we can use the accepts_nested_attributes_for to build our super and sub class at the same time:

Subscription.create(start_date: Date.today,
                    is_active: true,
                    was_couponed: false,
                    subscribable: 
                        MonthlySubscription.create(
                            expires_on: (Date.today + 31.days)
                        )
                    )

Putting it Into Practice

This let’s us use all kinds of helpful associations and queries between parent classes where the implementation details of the subclasses aren’t needed:

# Oh no, the stripe API was down. Retry them all
# without bothering the paypal customers
Subscription.payments
            .stripe_payments
            .where(was_successful: false)
            .each(&:retry_failed)

# Payment type doesn't matter if we use the same
# delegated_type pattern on Payments
MonthlySubscription.first
                   .payments
                   .stripe_payments #=> [StripePayment<#foobar>, ...]

MonthlySubscription.last
                   .payments
                   .paypal_payments #=> [PaypalPayment<#bazquux>, ...]

# Discounts
Subscription.first.subscribable.discount_amount #=> .2 // YearlySubscription
Subscription.last.subscribable.discount_amount #=> .1 // MonthlySubscription
Subscription.first.discounted_cost #=> 10099 cents
Subscription.last.discounted_cost #=> 999 cents

# What type of subscription was this charge for?
Payment.last.subscribable_type #=> "LifetimeSubscription"

# The same old #create we know and love...
Subscription.create(params[:subscription])

# ... supercharged with nested attributes via delegated_types
params = { subscription: { user_id: 16,
                           cost: 5000,
                           subscribable_type: "LifetimeSubscription",
                           subscribable_attributes: { was_gifted_lifetime_sub: true } } }

# Permit extra type params for nesting in strong params
def subscription_params
  params.require(:subscription)
    .permit(:user_id,
            :cost,
            :subscribable_type,
            subscribable_attributes: [
                :was_gifted_lifetime_sub,
                :monthly_coupon_code,
                :leap_year_registration
            ]
    )

There are many more niceties that come with using delegated_types so it’s a pattern worth checking out. In fact, there are still plenty of optimizations that could be done to this setup.

I would caution against using this approach as a first-resort, but I’m happy to use it on smaller apps where maintainers are very familiar with the codebase.

Again, the cost of implementing this pattern into your codebase is that you’ll have to do a bit more managing of some classes and concerns, but the big win is keeping your code clean, readable, and intuitive - things Rubyists love.

thanks

Thanks to Remi Mercier and his delegated_types blog post. Reading through someone’s thought process really helps build understanding for topics. Cheers! 37signals also has a thorough blog post about delegated types