Reputation: 8535
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
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
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
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 and
Tag -- and we need to tell rails which one to use.
Upvotes: 83