Unsure about correct association to use. has_many :through?

I’m trying to implement the following:

  • A Page has different ad spots. E.g. left and right.
  • There is a Campaign model representing ad campaigns.
  • Each ad spot can be filled with one Campaign at the time. E.g. Page 1 left has Campaign 1 attached. Page 1 right has also Campaign 1 attached.

I could just add left_campaign_id and right_campaign_id to the pages table and use belongs_to twice in the model. But since the amount of ad spots may change over time I feel like that’s not flexible enough.

So I was thinking about using a CampaignPlacement model with has_many :through. The migration would look like this:

class CreateCampaignPlacement < ActiveRecord::Migration[5.0]
  def change
    create_table :campaign_placements, id: :uuid do |t|
      t.uuid      :campaign_id,     null: false
      t.uuid      :page_id,         null: false
      t.integer   :ad_spot

The issue: I want to enforce only one campaign per Page and spot. And the user should select the campaign per spot through a select tag. This is where I’m starting to doubt if has_many :through is the correct approach. I looked at has_one :through as well, but doesn’t really seem to fit either.

Am I overlooking something? As things stand I’d probably keep using has_many :through and implement the logic to have only one relationship myself at the controller level. Feels wrong though.

Both approaches you described are perfectly fine. The belongs_to approach is definitely simpler, and I’d go with that unless you foresee frequent changes to the ad spots.

The campaign placement approach is great too, but if you go that route, you should use database-level unique constraints and/or ActiveRecord validations to enforce your rules, which should alleviate the “feels wrong” you might encounter by enforcing the rules at the controller level.

For example, at the database level, in your migration:

add_index :campaign_placements, [:page_id, :ad_spot], unique: true

At the ActiveRecord level:

validates :page_id, uniqueness: { scope: :ad_spot }

Yes belongs_to is much simpler and I could just throw the fields at simple_form and call it a day. However I forgot to mention one requirement:

I want to query all pages that show a certain campaign. Something that works better with the join table. So I’m giving this a try with the suggested unique validation and will see how it goes.