đ Another episode in the everyday adventures of a cowboy coder in rehab
In my previous post, I told you about that assignment that landed on my desk a few days ago. I talked about how I started modeling the database for a content API that came with some rather misleading requirements.
In todayâs episode, weâre going to dig a little deeper into one of the common characteristics shared by all content types.
Our streaming content API had big international ambitions. It was ready to conquer every platform in every countryâand it wasnât going to let a third-rate developer like me get in the way.
All the content types I modeled in the previous post had one thing in common: the availability property.
Here, I thought I could go for the simplest possible solution, right?
Well, said and almost done: availability turned out to be a hash with just two simple keys (Nice! sounds like a perfect JSON column).
One identifies the app or platform where the content is available using a string with its name, and the other does the same for the market.
Unless this is your first rodeo (and itâs not mine), at this point youâre probably seeing more red lights than a sinking submarine.
Itâs pretty clear that, at some pointâwhether itâs a stakeholder, a PM, or a designerâsomeoneâs going to ask for the ability to filter content by platform, by country, or by some combination of both.
And if you love JSON columns as much as I do, youâll know that filtering or querying them for anything slightly complex can quickly become yet another reason to move to the countryside and starve to death (because letâs be honestâyouâd love to have a farm, but you wouldnât know how to grow even a common cold).
So I say goodbye to all that dopamine that was about to be released as a reward for a small and quick win, and I take off my cowboy hat to put on my thinking one.
đ§© Modeling the entities
To start doing things properly, Iâll need a table for apps.
Even if, for now, they only have a name, trust meâsooner or later theyâll start getting attributes:
# app/models/app.rb
class App < ApplicationRecord
has_many :content_availabilities, dependent: :destroy
validates :name, presence: true, uniqueness: true
end
And weâll do the same for markets:
# app/models/market.rb
class Market < ApplicationRecord
has_many :content_availabilities, dependent: :destroy
validates :code, presence: true, uniqueness: true
end
I know, it looks like overkillâbut Rails migrations are free, right?
âïž ContentAvailability, the glue that holds it all together
Now that we have our two entities, the next step is to pair them up inside a ContentAvailability.
And this is where our dear old friend, the polymorphic association, comes back.
It lets us define a belongs_to relationship with different types of models (thanks to Rails magic), which is exactly what we need:
# app/models/content_availability.rb
class ContentAvailability < ApplicationRecord
belongs_to :content, polymorphic: true
belongs_to :app
belongs_to :market
validates :app_id, uniqueness: { scope: [:market_id, :content_type, :content_id],
message: "availability already exists for this app/market pair" }
end
And to close the loop, each content type (Movie, TvShow, Episode, etc.) includes the Contentable module, which already defines the polymorphic relationship:
# app/models/concerns/contentable.rb
has_many :content_availabilities, as: :content, dependent: :destroy
has_many :apps, through: :content_availabilities
has_many :markets, through: :content_availabilities
One last important detail is the constraint that prevents a content item from having more than one availability for the same app/market pair.
Thatâs reinforced at the database level too:
add_index :content_availabilities,
[:app_id, :market_id, :content_type, :content_id],
unique: true,
name: 'index_unique_content_availability'
đ€ Epilogue
And just like thatâwith a few lines of codeâwe turned what could have been a JSON column nightmare into an elegant, normalized, and easily extensible solution.
Could we have done it faster?
Yes.
Could we have done it worse?
Absolutely.
But honestly, thereâs nothing more satisfying than watching a design that looked like overkill on Monday save you from a massive refactor on Friday.
đ§© Full Code & Repository
You can check out the full implementation, including models, migrations, and specs, on GitHub:
Top comments (1)
Nice writeup. Did you think of linking content availabilities not directly with a polymorphic content, but to the
CatalogEntrymodel itself?I believe that could have worked just as well as in your previous post in this instance, don't you think?