markquezada
markquezada

Reputation: 8535

Possible to have "polymorphic has_one" relationship in rails?

I'd like to do something like this:

Category
--------
- id
- name

Tag
--------
- id
- tag


Campaign
--------
- id
- name
- target (either a tag *or* a category)

Is a polymorphic association the answer here? I can't seem to figure out how to use it with has_one :target, :as => :targetable.

Basically, I want Campaign.target to be set to a Tag or a Category (or potentially another model in the future).

Upvotes: 40

Views: 38915

Answers (3)

troelskn
troelskn

Reputation: 117507

The answers to this questions are great, but I just wanted to mention another way to accomplish the same. What you could do instead is create two relationships, eg.:

class Campaign < ActiveRecord::Base
  belongs_to :tag
  belongs_to :category
  validate :tag_and_category_mutually_exclusive

  def target=(tag_or_category)
    case
    when tag_or_category.kind_of?(Tag)
      self.tag = tag_or_category
      self.category = nil
    when tag_or_category.kind_of?(Category)
      self.category = tag_or_category
      self.tag = nil
    else
      raise ArgumentError, "Expected Tag or Category"
    end
  end

  def target(tag_or_category)
    tag || category
  end

  private 
  def tag_and_category_mutually_exclusive
    if tag && category
      errors.add "Can't have both a tag and a category"
    end
  end
end

The validation ensures that you don't accidentally end up with both fields set, and the target helpers allows polymorphic access to the tag/category.

The benefit of doing it like this is that you get a somewhat more correct database schema, where you can define proper foreign key constraints on the id columns. This will also lead to nicer and more efficient sql queries on the database level.

Upvotes: 7

user5390702
user5390702

Reputation: 11

Slight addendum: In the migration where you created the Campaign table, the t.references :target call should have :polymorphic => true (at least with rails 4.2)

Upvotes: 1

Kristian PD
Kristian PD

Reputation: 2695

I don't believe you're in need of a has_one association here, the belongs_to should be what you're looking for.

In this case, you'd want a target_id and target_type column on your Campaign table, you can create these in a rake with a t.references :target call (where t is the table variable).

class Campaign < ActiveRecord::Base
  belongs_to :target, :polymorphic => true
end

Now campaign can be associated to either a Tag or Category and @campaign.target would return the appropriate one.

The has_one association would be used if you have a foreign key on the target table pointing back to your Campaign.

For example, your tables would have

Tag: id, tag, campaign_id Category: id, category, campaign_id

and would have a belongs_to :campaign association on both of them. In this case, you'd have to use has_one :tag and has_one :category, but you couldn't use a generic target at this point.

Does that make more sense?

EDIT

Since target_id and target_type are effectively foreign keys to another table, your Campaign belongs to one of them. I can see your confusion with the wording because logically the Campaign is the container. I guess you can think of it as Campaign has a single target, and that's a Tag or a Container, therefore it belongs in a Tag or Container.

The has_one is the way of saying the relationship is defined on the target class. For example, a Tag would have be associated to the campaign through a has_one relationship since there's nothing on the tag class that identifies the association. In this case, you'd have

class Tag < ActiveRecord::Base
  has_one :campaign, :as => :target
end

and likewise for a Category. Here, the :as keyword is telling rails which association relates back to this Tag. Rails doesn't know how to figure this out upfront because there's no association with the name tag on the Campaign.

The other two options that may provide further confusion are the source and source_type options. These are only used in :through relationships, where you're actually joining the association through another table. The docs probably describe it better, but the source defines the association name, and source_type is used where that association is polymorphic. They only need to be used when the target association (on the :through class) has a name that isn't obvious -- like the case above with target andTag -- and we need to tell rails which one to use.

Upvotes: 83

Related Questions