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.
Imagine your web application has two important entities:
Post has a
title and a
Comment only has
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 :comments3end45class 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
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
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 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
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: integer3 timelineable_type: string4 timelineable_id: integer56posts:7 id: integer8 title: string9 content: text1011comments: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 TimelineItem2 belongs_to :timelineable, polymorphic: true3end45class Post6 has_one :timeline_item, as: timelineable7end89class 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 TimelineItem2 belongs_to :timelineable, polymorphic: true34 def title5 timelineable.title6 end78 def content9 timelineable.content10 end11end1213class Post14 has_one :timeline_item, as: timelineable15end1617class Comment18 has_one :timeline_item, as: timelineable1920 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 TimelineItem2 delegated_types :timelineable, types: %w[ Post Comment ]34 def title5 timelineable.title6 end78 def content9 timelineable.content10 end11end1213class Post14 has_one :timeline_item, as: timelineable15end1617class Comment18 has_one :timeline_item, as: timelineable1920 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:
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! :-)