How to retrieve attributes from has_one association without belongs_to?

437 views Asked by At

I have several nested classes that look like so:

class Presentation < ActiveRecord::Base
  has_many :campaign_records, :as => :campaignable

  def campaign_records_text(joiner)
    return '' if self.campaign_records.blank?
    self.campaign_records.map {|c| c.to_s}.join(joiner)
  end
end

class CampaignRecord < ActiveRecord::Base
  belongs_to :campaignable, :polymorphic => true
  has_one :campaign
  has_one :tier_one
  has_one :tier_two

  def to_s
    "#{campaign.name} - #{tier_one.name} - #{tier_two.name}"
  end
end

class Campaign < ActiveRecord::Base
   has_many :tier_ones

   attr_accessible :name
end

class TierOne < ActiveRecord::Base
   has_many :tier_twos
   belongs_to :campaign

   attr_accessible :name
end

class TierTwo < ActiveRecord::Base
   belongs_to :tier_one

   attr_accessible :name
end

In summary, a Campaign has many associated TierOnes and every TierOne has it's own set of TierTwos. A Presentation has a set of CampaignRecords which link a Campaign,TierOne, and TierTwo together. Note though that a Campaign does not belong_to a CampaignRecord because many CampaignRecords can refer to it.

So here's the problem: I want to change the CampaignRecord.to_s method to return "campaign.name - tier_one.name - tier_two.name" (like shown above) but doing so results in an error when I try to call some_campaign_record.to_s:

ActionView::Template::Error (Mysql2::Error: Unknown column 'campaigns.campaign_record_id' in 'where clause': SELECT 'campaigns'.* FROM 'campaigns' WHERE 'campaigns'.'campaign_record_id' = # LIMIT 1)

Where did I go wrong here? I know that rails auto generates a lot of getters and setters for me, but the default to_s method is just the usual so how do I override it in the proper rails way? Does a has_one require a belongs_to or is there a belongs_to_many hiding out there somewhere that I should have used instead?

Any help would be very much appreciated! Thanks in advance!

(Also, I saw that my question is very similar to this unanswered question)

EDIT

I'm seeing a bit of confusion about the model structure here so let me try to explain it differently in a way that will hopefully be clearer.

First off, just to be clear a Campaign is very different from a CampaignRecord.

Think of the Campaign-TierOne-TierTwo relationship like a three layered list:

  1. Campaign 1
    • TierOne 1.1
      • TierTwo 1.1.1
      • TierTwo 1.1.2
      • ...
    • TierOne 1.2
      • TierTwo 1.2.1
      • TierTwo 1.2.2
      • ...
    • ...
  2. Campaign 2
    • TierOne 2.1
      • TierTwo 2.1.1
      • TierTwo 2.1.2
      • ...
    • TierOne 2.2
      • TierTwo 2.2.1
      • TierTwo 2.2.2
      • ...
    • ...
  3. Campaign 3 ...

The CampaignRecord model is a representation of a Campaign, TierOne, TierTwo tuple. When its first created, you select a Campaign. Then select a TierOne from that Campaign's set of TierOnes. Then a TierTwo from that TierOne's set of TierTwos. In other words, the CampaignRecord model is a path which traverses a Campaign-TierOne-TierTwo tree.

The set of presentation.campaign_records is the set of valid Campaign-TierOne-TierTwo paths which a user has previously associated with that presentation instance. (A Presentation will have zero or more of these paths associated with it.)

The important bit of functionality is that a Presentation should have a variable size set of Campaign-TierOne-TierTwo linkages. While modifying any given Presentation, I need to be able to modify/add/remove Campaign-TierOne-TierTwo linkages to/from a Presentation. I chose to represent those Campaign-TierOne-TierTwo linkages as CampaignRecords. A Presentation can have a bunch of these CampaignRecords, but no Campaign, TierOne, or TierTwo will ever belong_to a CampaignRecord.

So, my question becomes: Why is my Campaign model throwing a "can't find specified column" error when it was never supposed to be looking for that column in the first place?

@presentations = Presentations.all
@presentations.each do |presentation|
    presentation.campaign_records.each do |campaign_record|
        print campaign_record.to_s # Campaign model throws error here
    end
 end
2

There are 2 answers

3
Lanny Bose On

Based on the comments above, one error I see is the following:

class CampaignRecord < ActiveRecord::Base
  ...
  has_one :tier_two
  ...
end

Take a look at the Rails Guide on Associations.. Campaign is going to expect to see a campaign_record_id in any ActiveRecord model that has a has_something association.

My best bet is that you'll want to add that column into the TierTwo database.

Alternately, if every TierTwo's CampaignRecord can be inferred through its TierOne, you could also hypothetically do:

class CampaignRecord < ActiveRecord::Base
  ...
  has_many :tier_twos, through: :tier_one
  ...
end

However, that doesn't seem like the right option. You say a CampaignRecord has only one TierTwo, and TierOne can have many TierTwos.

Let me know if this helps. It's possible I'm missing some information about your business logic that would help clarify my recommendation.

0
ianking On

Solution seems pretty obvious to me now, in hindsight. My CampaignRecord model had fields to store IDs of other model records. But the has_one in the CampaignRecord implies that other models should be storing the ID of the CampaignRecord. Solution was to change the has_ones to belongs_tos so that the lookup goes in the other direction:

class CampaignRecord < ActiveRecord::Base
  belongs_to :campaignable, :polymorphic => true
  belongs_to :campaign
  belongs_to :tier_one
  belongs_to :tier_two

  def to_s
    "#{campaign.name} - #{tier_one.name} - #{tier_two.name}"
  end
end