how to emulate has_many :through with polymorphic classes - ruby-on-rails

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 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

Related

Using ActiveRelation with :joins on polymorphic has_one to implicitly create relation

We can use ActiveRelation like this:
MyModel.where(:field => "test").create => #<Message ... field:"test">
But it doesnt work for joins with polymorphic has_one associations:
class RelatedModel < AR::Base
# has :some_field
belongs_to :subject, :polymorphic => true
end
class MyModel < AR::Base
# need some dirty magic here
# to build default related_model with params from active_relation
has_one :related_model, :as => :subject, :dependent => :destroy
end
describe MyModel do
it "should auto-create has_one association with joins" do
test = MyModel.joins(:related_model).where("related_models.subject_type" => "MyModel", "related_models.some_field" => "chachacha").create
test.related_model.should_not be_nil
test.related_model.some_field.should == "chachacha"
test.related_model.subject_type.should == "MyModel"
test.related_model.subject_id.should == test.id
# fails =)
end
end
Is it possible to extract active_relation params, pass them to MyModel for use in before_create and build RelatedModel with them?
Diving into ActiveRecord sources i found that
ActiveRecord::Relation covers 'create' with 'scoping' method.
ActiveRecord::Persistance 'create' calls 'initialize' from ActiveRecord::Core.
ActiveRecord::Core 'initialize' calls 'populate_with_current_scope_attributes'
This method declared in ActiveRecord::Scoping uses 'scope_attributes' declared in ActiveRecord::Scoping::Named.
scope_attributes creating relation 'all' and calls 'scope_for_create' on it.
'ActiveRecord::Relation's 'scope_for_create' uses only 'where_values_hash' from current_scope that does not contain rules like 'related_models.subject_type' (this values are contained in where_clauses). So we need to have simple key-value wheres to be used with 'create' on ActiveRecord::Relation. But ActiveRecord not clever enough to know that 'some_field' in where clause should be used with join table.
I found it can be implemented only by accessing where options with self.class.current_scope.where_clauses in 'before_create' on MyModel, parsing them and setting up attributes.
class MyModel < AR::Base
before_create :create_default_node
def create_default_node
clause = self.class.current_scope.where_clauses.detect{|clause| clause =~ /\`related_models\`.\`some_field\`/}
value = clause.scan(/\=.+\`([[:word:]]+)\`/).flatten.first
self.create_node(:some_field => value)
end
end
But it is so dirty, then i decided to find simpler solution and inverted dependency as described in Railscast Pro #394, moved RelatedModel functionality to MyModel with STI. Actually i needed such complicated relation creation because RelatedModel had some functionality common for all models (acts as tree). I decided to delegate 'ancestors' and 'children' to RelatedModel. Inverting dependency solved this problem.
class MyModel < AR::Base
acts_as_tree
belongs_to :subject, :polymorphic => true
end
class MyModel2 < MyModel
end
class RelatedModel < AR::Base
# has :some_field
has_one :my_model, :as => :subject, :dependent => :destroy
end
MyModel.create{|m| m.subject = RelatedModel.create(:some_field => "chachacha")}
MyModel.ancestors # no need to proxy relations

how to find out active record property through many to many

I'm currently adjusting fedena to have a many:many relationship between students and guardians (as opposed to one:many student:guardians).
So this is what I did:
class Guardian < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :students, :through=>:parentings
end
class Student < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :guardians, :through=>:parentings
end
class Parenting < ActiveRecord::Base
attr_accessible :student_id, :guardian_id
belongs_to :student
belongs_to :guardian
end
inside guardian.rb there was this class method:
def self.shift_user(student)
# find all the guardians having a ward_id = student.d (comment my own)
self.find_all_by_ward_id(student.id).each do |g|
..
end
I want to change it using the newly defined relationshop ie
self.find_all_by_student_id(student.id).each do |g|
..
It doesn't work! I thought it would work since I've already defined that a Guardian has many students through the Parenting class.. I've tried several permutations of the command above and I keep on getting the error:
undefined method `find_all_by_student_id' for #<Class:0x1091c6b28>
ideas? I'm using ruby 1.8.7 and RoR 2.3.5
Guardian has no propety student_id so there is no method find_all_by_student_id. So I don't understand why you are confused. Why don't you just use student.guardians?
You can do this using a named scope and a more complex query
class Guardian < ActiveRecord::Base
has_many :parentings, :dependent=>:destroy
has_many :students, :through=>:parentings
named_scope :find_all_by_student_id, lambda {|student_id|
{ :all,
:select => "guardians.*",
:joins => "JOIN parentings ON parentings.guardian_id = guardians.id
JOIN students ON students.id = parentings.student_id",
:conditions = ["students.id = ?", student_id] } }
end

(Rails Question) Merging multiple polymorphic has_many relationships

(This is not the actual code I'm using, although this sums up the idea of what I want to do)
class Connection < ActiveRecord::Base
belongs_to :connection1, :polymorphic => true
belongs_to :connection2, :polymorphic => true
end
class User < ActiveRecord::Base
has_many :followers, :class_name => 'Connection', :as => :connection1
has_many :followings, :class_name => 'Connection', :as => :connection2
end
My question is that I want to know how I will be able to create a method called "network" such that what is returned isn't an array. Like so,
u = User.first
u.network # this will return a merged version of :followings and :followers
So that I'll still be able to do this:
u.network.find_by_last_name("James")
ETA:
Or hmm, I think my question really boils down to if it is possible to create a method that will merge 2 has_many associations in such a way that I can still call on its find_by methods.
Are you sure that you want a collection of Connections, rather than a collection of Users?
If it's a collection of Connections that you need, it seems like you'll be well served by a class method on Connection (or scope, if you like such things).
connection.rb
class Connection < ActiveRecord::Base
class << self
def associated_with_model_id(model, model_id)
include([:connection1, :connection2]).
where("(connection1_type IS #{model} AND connection1_id IS #{model_id})
OR (connection2_type IS #{model} AND connection2_id IS #{model_id})")
end
end
end
user.rb
class User < ActiveRecord::Base
def network
Connection.associated_with_model_id(self.class.to_s, id)
end
end
Probably not as useful as you'd like, but maybe it'll give you some ideas.

How to model has_many with polymorphism?

I've run into a situation that I am not quite sure how to model.
EDIT: The code below now represent a working solution. I am still interested in nicer looking solutions, though.
Suppose I have a User class, and a user has many services. However, these services are quite different, for example a MailService and a BackupService, so single table inheritance won't do. Instead, I am thinking of using polymorphic associations together with an abstract base class:
class User < ActiveRecord::Base
has_many :services
end
class Service < ActiveRecord::Base
validates_presence_of :user_id, :implementation_id, :implementation_type
validates_uniqueness_of :user_id, :scope => :implementation_type
belongs_to :user
belongs_to :implementation, :polymorphic => true, :dependent => :destroy
delegate :common_service_method, :name, :to => :implementation
end
#Base class for service implementations
class ServiceImplementation < ActiveRecord::Base
validates_presence_of :user_id, :on => :create
#Virtual attribute, allows us to create service implementations in one step
attr_accessor :user_id
has_one :service, :as => :implementation
after_create :create_service_record
#Tell Rails this class does not use a table.
def self.abstract_class?
true
end
#Name of the service.
def name
self.class.name
end
#Returns the user this service
#implementation belongs to.
def user
unless service.nil?
service.user
else #Service not yet created
#my_user ||= User.find(user_id) rescue nil
end
end
#Sets the user this
#implementation belongs to.
def user=(usr)
#my_user = usr
user_id = usr.id
end
protected
#Sets up a service object after object creation.
def create_service_record
service = Service.new(:user_id => user_id)
service.implementation = self
service.save!
end
end
class MailService < ServiceImplementation
#validations, etc...
def common_service_method
puts "MailService implementation of common service method"
end
end
#Example usage
MailService.create(..., :user => user)
BackupService.create(...., :user => user)
user.services.each do |s|
puts "#{user.name} is using #{s.name}"
end #Daniel is using MailService, Daniel is using BackupService
Notice that I want the Service instance to be implictly created when I create a new service.
So, is this the best solution? Or even a good one? How have you solved this kind of problem?
I don't think your current solution will work. If ServiceImplementation is abstract, what will the associated classes point to? How does the other end of the has_one work, if ServiceImplementation doesn't have a pk persisted to the database? Maybe I'm missing something.
EDIT: Whoops, my original didn't work either. But the idea is still there. Instead of a module, go ahead and use Service with STI instead of polymorphism, and extend it with individual implementations. I think you're stuck with STI and a bunch of unused columns across different implementations, or rethinking the services relationship in general. The delegation solution you have might work as a separate ActiveRecord, but I don't see how it works as abstract if it has to have a has_one relationship.
EDIT: So instead of your original abstract solution, why not persist the delgates? You'd have to have separate tables for MailServiceDelegate and BackupServiceDelegate -- not sure how to get around that if you want to avoid all the null columns with pure STI. You can use a module with the delgate classes to capture the common relationships and validations, etc. Sorry it took me a couple of passes to catch up with your problem:
class User < ActiveRecord::Base
has_many :services
end
class Service < ActiveRecord::Base
validates_presence_of :user_id
belongs_to :user
belongs_to :service_delegate, :polymorphic => true
delegate :common_service_method, :name, :to => :service_delegate
end
class MailServiceDelegate < ActiveRecord::Base
include ServiceDelegate
def name
# implement
end
def common_service_method
# implement
end
end
class BackupServiceDelegate < ActiveRecord::Base
include ServiceDelegate
def name
# implement
end
def common_service_method
# implement
end
end
module ServiceDelegate
def self.included(base)
base.has_one :service, :as => service_delegate
end
def name
raise "Not Implemented"
end
def common_service_method
raise "Not Implemented"
end
end
I think following will work
in user.rb
has_many :mail_service, :class_name => 'Service'
has_many :backup_service, :class_name => 'Service'
in service.rb
belongs_to :mail_user, :class_name => 'User', :foreign_key => 'user_id', :conditions=> is_mail=true
belongs_to :backup_user, :class_name => 'User', :foreign_key => 'user_id', :conditions=> is_mail=false

Is there a way of doing filtering joined associations using named scope?

I have the following associated models
class Enrollment < ActiveRecord::Base
has_many :addresses
end
class Address < ActiveRecord::Base
belongs_to :address_type
end
Currently I'm using the following (which I think is ugly) to filter out enrollment addresses of a certain address type.
class Enrollment < ActiveRecord::Base
def local_address
adds = []
addresses.each do |add|
adds << add if add.address_type.name == 'Local'
end
adds.last
end
end
Is there a way of using named scope of doing the same thing?
A generic solution:
class Address < ActiveRecord::Base
belongs_to :address_type
named_scope :local, { :conditions => { :address_type => { :name => "Local" }}}
end
This allows you to do the following:
Enrollment.find(12).addresses.local # Association extended with .local method
Address.local.all # Class methods extended with .local method
The named scope could help in all situations where you are only using "local" addresses.
With reference from the following stackoverflow post, I managed to solved my named scope query
Rails named_scopes with joins
Basically I need to do joins in the query
class Address < ActiveRecord::Base
belongs_to :address_type
named_scope :local, {
:joins => "INNER JOIN address_types ON address_types.id = addresses.address_type_id",
:conditions => "address_types.name = 'Local'"
}
end
So effectively I can rewrite my Enrollment's "local_address" method to
clss Enrollment < ActiveRecord::Base
def local_address
addresses.local.last
end
end

Resources