DEV Community

Cover image for Two wrongs doesn't make a Polymorphic.
develaper
develaper

Posted on

Two wrongs doesn't make a Polymorphic.

🐎 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

đŸ€  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:

👉 stream-content-api

Top comments (1)

Collapse
 
nfilzi profile image
Nicolas Filzi

Nice writeup. Did you think of linking content availabilities not directly with a polymorphic content, but to the CatalogEntry model itself?

I believe that could have worked just as well as in your previous post in this instance, don't you think?