开发者

how to emulate has_many :through with polymorphic classes

开发者 https://www.devze.com 2023-03-29 07:49 出处:网络
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 开

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
0

精彩评论

暂无评论...
验证码 换一张
取 消