Delegated Types don’t work the way I thought they did

Scenario

We’re planning out some modelling where Users can create different types of Posts.

We have a regular Post, an Article and a Video.

Each of these Posts should be displayed in some sort of timeline.

Planning

We iterate through the planning process.

Iteration 1

Keeping everything separate is a good place to start.

erDiagram

Post {
    uuid user_id FK
    text content
}

Article {
    uuid user_id FK
    string title
    text content
}

Video {
    uuid user_id FK
    string title
    text content
    string source_path
}

Displaying each Post on a Timeline starts to look like

user = current_user

timeline_items = []
timeline_items << user.posts
timeline_items << user.articles
timeline_items << user.videos

This is not a good way to get all of the posts. We’d probably move this into the User model, but what if we want to sort these posts from newest to oldest? Now we’re asking Ruby to do the sorting and at the medium1 scale we’re going to have performance issues.

Iteration 2

This one is tricky - we’d rather have a single Post table and have types of Posts.

As soon as a developer mentions “type of…” we default to Single Table Inheritance (STI).

With that in mind, we plan the following structure…

classDiagram
direction RL

class Post {
    uuid user_id FK
    string type  "Rails-way of handling STI"
    string title "nullable"
    text content
    string source_path "nullable"
}

Post <|-- Article
Post <|-- Video

This set-up helps with our Ruby code for sorting User posts for the Timeline.

user = current_user

timeline_items = user.posts

There are drawbacks to this implementation.

When we create a regular Post, the database row will have empty columns.

user_id type title content source_path
1234-122 “Post” null “This is a post” null

Not a huge issue for now but again as the post types expand, we could have a row in the database with very little data.

Iteration 3

Delegated types - this allows us to have one table for all the shared data and individual tables for the types of Posts.

That looks more like

erDiagram

Post {
    uuid user_id FK
    text content
    string postable_type
    uuid postable_id
}

Article {
    string title
}

Video {
    string title
    string source_path
}

In Rails the classes are set up like


class Post < ActiveRecord
  delegated_type :postable, types: %w[Article Video]
end

class Article < ActiveRecord
  has_one :post, as: :postable
end

class Video < ActiveRecord
  has_one :post, as: :postable
end

And again our Timeline code looks sensible

user = current_user

timeline_items = user.posts

But now creating a new Article or Video is more complicated than it used to be.

It used to be simple like

article = Article.create(user: current_user, layout: post.liquid
title: "Some Title", content: "More content")

This won’t work with delegated types in place. We must create the Post, and an Article and then link them together.

article = Article.create(layout: post.liquid
title: "Some Title")
post = Post.create(user: current_user, content: "More content", postable: article)

Summary

This is where delegated types let us down. Creating a record becomes difficult, with the developer having to know which attributes live on each model.

Before, with STI, we could create a new Article easily by passing in all the arguments to one create method.

Now we have to consider wrapping this functionality in another model (PORO) and we have to consider failures in either create method - this is usually solved with transactions.

We would expect delegated types to behave the same as an STI implementation, but with the additional separation of data.

Possible solutions

Adding a method on the Post


class Post < ActiveRecord
  delegated_type :postable, types: %w[Article Video]

  def self.post!(user:, content:, postable:)
    transaction do
      postable.save!
      Post.create!(user:, content:, postable:)
    end
  end
end

article = Article.create(layout: post.liquid
title: "Some Title")
post = Post.post!(user: current_user, content: "Some content", postable: article)

Accepts Nested Attributes


class Article < ActiveRecord
  has_one :post, as: :postable
  accepts_attributes_for :post
end

article = Article.create(
  layout: post.liquid
title: "Some Title",
  post_attributes: {
    user: current_user,
    content: "Some content"
  }
)

Unfortunately, this approach does not work. The create method on the Post is called first, which causes a validation error as the postable (Article) is not set and has not been saved.

  1. I have no idea what medium scales is - 100+ posts 🤷