how to emulate has_many :through with polymorphic classes
I understand why ActiveRecord can't support has_many :through
on polymorphic classes. But I would like to emulate some of its functionality. Consider the following, where a join table associates two 开发者_Python百科polymorphic classes:
class HostPest < ActiveRecord::Base
belongs_to :host, :polymorphic => true
belongs_to :pest, :polymorphic => true
end
class Host < ActiveRecord::Base
self.abstract_class = true
has_many :host_pests, :as => :host
end
class Pest < ActiveRecord::Base
self.abstract_class = true
has_one :host_pest, :as => :pest
end
class Dog < Host ; end
class Cat < Host ; end
class Flea < Pest ; end
class Tick < Pest ; end
The goal
Since I can't do has_many :pests, :through=>:host_pests, :as=>:host
(etc), I'd like to emulate these four methods:
dog.pests (returns a list of pests associated with this dog)
flea.host (return the host associated with this flea)
cat.pests << Tick.create (creates a HostPest record)
tick.host = Cat.create (creates a HostPest record)
Question 1
I've got a working implementation for the first two methods (pests
and host
), but want to know if this is the best way (specifically, am I overlooking something in ActiveRecord associations that would help):
class Host < ActiveRecord::Base
def pests
HostPest.where(:host_id => self.id, :host_type => self.class).map {|hp| hp.pest}
end
end
class Pest < ActiveRecord::Base
def host
HostPest.where(:pest_id => self.id, :pest_type => self.class).first.host
end
end
Question 2
I'm stumped on how to implement the <<
and =
methods implied here:
cat.pests << Tick.create # => HostPest(:host=>cat, :pest=>tick).create
tick.host = Cat.create # => HostPest(:host=>cat, :pest=>tick).create
Any suggestions? (And again, can ActiveRecord associations provide any help?)
Implementing the host=
method on the Pest
class is straight forward. We need to make sure we clear the old host while setting a new host (as AR doesn't clear the old value from the intermediary table.).
class Pest < ActiveRecord::Base
self.abstract_class = true
has_one :host_pest, :as => :pest
def host=(host)
Pest.transaction do
host_pest.try(:destroy) # destroy the current setting if any
create_host_pest(:host => host)
end
end
end
Implementing pests<<
method on Host
class is bit more involved. Add the pests
method on the Host
class to return the aggregated list of pests. Add the <<
method on the object returned by pests
method.
class Host < ActiveRecord::Base
self.abstract_class = true
has_many :host_pests, :as => :host
# pest list accessor
def pests
@pests ||= begin
host = self # variable to hold the current self.
# We need it later in the block
list = pest_list
# declare << method on the pests list
list.singleton_class.send(:define_method, "<<") do |pest|
# host variable accessible in the block
host.host_pests.create(:pest => pest)
end
list
end
end
private
def pest_list
# put your pest concatenation code here
end
end
Now
cat.pests # returns a list
cat.pests << flea # appends the flea to the pest list
You can address your problem by using STI and regular association:
class HostPest < ActiveRecord::Base
belongs_to :host
belongs_to :pest
end
Store all the hosts in a table called hosts
. Add a string column called type
to the table.
class Host < ActiveRecord::Base
has_many :host_pests
has_many :pests, :through => :host_pests
end
Inherit the Host
class to create new hosts.
class Dog < Host ; end
class Cat < Host ; end
Store all the pests in a table called pests
. Add a string column called type
to the table.
class Pest < ActiveRecord::Base
has_one :host_pest
has_one :host, :through => :host_pest
end
Inherit the Pest
class to create new pests.
class Flea < Pest ; end
class Tick < Pest ; end
Now when you can run following commands:
dog.pests (returns a list of pests associated with this dog)
flea.host (return the host associated with this flea)
cat.pests << Tick.create (creates a HostPest record)
tick.host = Cat.create (creates a HostPest record)
Note
Rails supports has_many :through
on polymorphic classes. You need to specify the source_type
for this to work.
Consider the models for tagging:
class Tag
has_many :tag_links
end
class TagLink
belongs_to :tag
belongs_to :tagger, :polymorphic => true
end
Let's say products and companies can be tagged.
class Product
has_many :tag_links, :as => :tagger
has_many :tags, :through => :tag_links
end
class Company
has_many :tag_links, :as => :tagger
has_many :tags, :through => :tag_links
end
We can add an association on Tag model to get all the tagged products as follows:
class Tag
has_many :tag_links
has_many :products, :through => :tag_links,
:source => :tagger, :source_type => 'Product'
end
精彩评论