Recently, DHH published this tweet about adding the delegated type to Rails. I wrote a thread explaining what it did, but I wanted to write a blog post to explain it better.
First, you may use this without knowing. This is just the use of a polymorphic relation to certain specific cases. The example are going to be in in some pseudo-Rails, as all I need is for you to understand the concept.
The problem
Imagine your web application has two important entities: Posts
and Comments
.
A Post
has a title
and a content
columns.
A Comment
only has content
.
The relations are pretty simple here: a Comment
belongs to a Post
and a Post
has many Comments
. Here’s some code:
1class Post2 has_many :comments3end4 5class Comment6 belongs_to :post7end
Now, imagine you want to create a timeline, that, for now, shows both posts and comments. How would you do this?
Well, you could fetch posts
and comments
and then write some logic to merge them and show them chronologically. It does work, but it’s a bit messy.
Things start to get more complicated once you start designing the UI: first, you will have to check which type of resource you are dealing with: is it a Post
or a Comment
?
Remember they don’t have the same columns, so you will have to write some logic to handle that. You will also have to have some conditional blocks to determine where to link to, etc.
Well, if you want to add pagination it becomes even trickier. It’s just not a really good solution.
The solution
The most common solution is creating a super-table and letting your app find out if it’s a Post or a Comment.
So, instead of having these two tables, you would a single timeline_items
table or something with a type
column to determine which type of it is, plus all the columns an item may have.
Remember they don’t necessarily have the same columns? That means your supertable would need all of them and only use a few ones at a time.
1timeline_items:2 id: integer3 title: text nullable4 content: text nullable5 type: string6 author_id: integer
We’re dealing with only two resources here. If we were to add another timeline item, we would have to add it’s columns there again. It gets messy.
In your template, you would have to check what type of item you’re dealing with, pull certain attributes from it, etc. It’s not a good solution. To ease things up, you could divide those into several models and use single-table inheritance, but then you would stumble on our first problem — you would still have to deal with different models, and if you were to fetch the parent model, you would still have a messy table and a bunch of logic.
The “correct” solution
Rails now has this natively as “delegated types”. For those who have used Laravel or Rails, it’s somewhat a polymorphic relation. The difference is we usually use polymorphic relations in a Parent > Child manner.
For instance, if you have a images
table in your app that can belong to either a Post
or a Comment
, the common thing is to fetch the Post
or Comment
and then get it’s images.
Now, we are doing the opposite. The polymorphable model is now a wrapper that we can use to fetch multiple models at once.
It would be like fetching all the images and showing it alongside their post or comment in a timeline.
You create a parent table which holds information shared amongst all children, and the children only include data particular do them. For instance, using our previous example, we would end up with these 3 tables:
1timeline_items: 2 id: integer 3 timelineable_type: string 4 timelineable_id: integer 5 6posts: 7 id: integer 8 title: string 9 content: text10 11comments:12 id: integer13 content: text
Notice that we don’t have a giant single-table anymore — we do still have the two tables from the first example but now they all reference a parent.
What we can do now is, instead of querying two different tables to show our time, query the timeline_items
table and then access the related model. Imagine we are fetching them through a “middleman”.
1class TimelineItem 2 belongs_to :timelineable, polymorphic: true 3end 4 5class Post 6 has_one :timeline_item, as: timelineable 7end 8 9class Comment10 has_one :timeline_item, as: timelineable11end
You can even go further and write methods in the TimelineItem
so you don’t have to even write conditional logic.
1class TimelineItem 2 belongs_to :timelineable, polymorphic: true 3 4 def title 5 timelineable.title 6 end 7 8 def content 9 timelineable.content10 end11end12 13class Post14 has_one :timeline_item, as: timelineable15end16 17class Comment18 has_one :timeline_item, as: timelineable19 20 def title21 content.truncate(20)22 end23end
You could the same with only polymorphic relationships, but now you have a way for Rails to handle things to you.
Basically, you’ll retrieve “timeline items” (though you still can retrieve posts and comments individually), and, if you’d like, write one unique way to handle them all.
See that while a post title is simply it’s title
column, a Post
title is a truncated version of it’s content. Timeline items can delegate things like figuring out the title to their related resource instead, etc.
One small, annoying thing of this is that, for instance, when creating a comment, you’d also have to create a timeline_item after it.
Want to query timeline items that are Posts
? You’ll have to write a scope.
Check if a timeline item is a Comment
? You will have to write your logic.
What Rails now does is natively implement this.
If we were simply to change the belongs_to
in the TimelineItem
model for delegated_type
, we get access to several cool things.
1class TimelineItem 2 delegated_types :timelineable, types: %w[ Post Comment ] 3 4 def title 5 timelineable.title 6 end 7 8 def content 9 timelineable.content10 end11end12 13class Post14 has_one :timeline_item, as: timelineable15end16 17class Comment18 has_one :timeline_item, as: timelineable19 20 def title21 content.truncate(20)22 end23end
There are lots of added methods, but for instance, you could check if a timeline item is a post like this: timeline_item.post?
, or you could limit your query to Comment timelineables like this: TimelineItem.posts
.
There are lots of cool things and DHH’s PR has all them listed on the documentation. I suggest you check it out!
Hope to have been of any help! :-)