RunFor
RunFor

Reputation: 555

Double polymorphic association

In my rails app I have two models: Person and Company. I need to specify many-to-many relationship between any pairs of this objects. So I should be able do like this:

@connections = @person.connections

where @connections is an array of Person and Company objects.

Right now I've created ConnectionBinding model for this and it does not work as wanted:

class ConnectionBinding < ActiveRecord::Base
  belongs_to :connect_from, polymorphic: true
  belongs_to :connect_to,   polymorphic: true
end

It thougths ActiveRecord::HasManyThroughAssociationPolymorphicSourceError exception

Does anybody already solve this problem? Any suggestions appreciated.

Upvotes: 3

Views: 2024

Answers (3)

futbolpal
futbolpal

Reputation: 1388

A Concern:

module Linkable
  extend ActiveSupport::Concern

  included do
    has_many :links_to, as: :linkable_to, class_name: 'Link' # [this]->[other object]
    has_many :links_from, as: :linkable_from, class_name: 'Link' # [other object]->[this]
  end
end

Links Table:

class Link < ActiveRecord::Base

  #The idea for this class is to by a double polymorphic table, linking an object to another object
  belongs_to :linkable_from, :polymorphic => true
  belongs_to :linkable_to, :polymorphic => true
end

Upvotes: 0

RunFor
RunFor

Reputation: 555

Thanx Chris for showing the idea. But I need to do some changes to make this work. The problem of Chris example is that if I call person_connections method it will generate wrong query.

Person.find(720).person_connections

generates this SQL

SELECT  "people".* FROM "people"
INNER JOIN "connection_bindings"
  ON "people"."id" = "connection_bindings"."connect_to_id"
WHERE "connection_bindings"."person_id" = 720
  AND "connection_bindings"."connect_to_type" = 'Person'

That person_id column should be connect_from_id. So I need to add :as => :connect_from option to has_many :connection_bindings to show ActiveRecord that its a polymorphic association.

has_many :connection_bindings, :as => :connect_from
has_many :person_connections,  :through => :connection_bindings,
  :source => :connect_to, source_type: 'Person'
has_many :company_connections, :through => :connection_bindings,
  :source => :connect_to, source_type: 'Company'

But it is still not enough. I need to be able to get reverse connections. So if Person @a adds connection to Person @b? method connections should show that connection in both directions @a.connections and @b.connections.

And now I think I can do this by adding couple of additional associations and aggregation methods.

Upvotes: 2

Chris
Chris

Reputation: 12181

You need to tell ActiveRecord the associated column that it is looking for. I'm guessing you want to have the following:

class Person < ActiveRecord::Base
  has_many :connection_bindings
  has_many :companies, :through => :connection_bindings
  has_many :people, :through => :connection_bindings
end

class company < ActiveRecord::Base
  has_many :connection_bindings
  has_many :companies, :through => :connection_bindings
  has_many :people, :through => :connection_bindings
end

The problem there is that you have two tables putting there id in one column and Rails doesn't know which table to look up.

For example, on any given connection_binding row in the database, the connect_from can be either a company_id or a person_id and the same is true of connect_to. So you say: 'Hey Rails, load up my associated ConnectionBindings' and it gets a row where the connect_from is 11 and the connect_to is 12. But does it do Person.find(12) or Company.find(12)? There is no way to tell!

Instead, you'll have to give Rails some more information:

class Person < ActiveRecord::Base
  has_many :connection_bindings
  has_many :person_connections, :through => :connection_bindings, :source => :to_connect, :source_type => 'Person'
  has_many :company_connections, :through => :connection_bindings, :source => :to_connect, :source_type => 'Company

  def connections
    person_connections + company_connections
  end
end

You'll need to build that on the other side (as well as the associated :from_connect's) but it depends on how you're using these connections. Should be enough to get you started.

It is a lot more typing than you're used to in the magical world of Ruby and Rails, but it is a very complex data pattern that you're trying to construct. That doesn't mean it is impossible – Rails, as a good framework, won't stop you from doing anything you really want to – but it is uncommon enough to require some explicitness on your end.

Upvotes: 7

Related Questions