Question

I have a relationship table :

create_table "animal_friends", :force => true do |t|
    t.integer  "animal_id"
    t.integer  "animal_friend_id"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.integer  "status_id",        :default => 1
  end

linking animals to others. Best way to retreive associations in SQL is :

SELECT animals.* 
from animals join animal_friends as af 
  on animals.id = 
    case when af.animal_id = #{id} then af.animal_friend_id else af.animal_id end 
WHERE #{id} in (af.animal_id, af.animal_friend_id)

And I can't find a way to create a proper has_many relation in rails with this. Apparently, there's no way to provide joining conditions for has_many.

I'm currently using a finder_sql :

has_many :friends, :class_name => "Animal", :finder_sql => 'SELECT animals.* from animals join animal_friends as af on animals.id = case when af.animal_id = #{id} then af.animal_friend_id else af.animal_id end ' +
 'WHERE #{id} in (af.animal_id, af.animal_friend_id) and status_id = #{Status::CONFIRMED.id}'  

but this method has the great disadvantage of breaking activerecord magic. For instance :

@animal.friends.first 

will execute the finder_sql without limit, fetching thousands of rows, then taking the first of the array (and loosing several precious seconds / req).

I guess it's a missing feature from AR, but I'd like to be sure first :) Thanks

Was it helpful?

Solution

You could solve this on the database level with a view, which would be the correct method anyway.

CREATE VIEW with_inverse_animal_friends (
  SELECT id,
         animal_id,
         animal_friend_id,
         created_at,
         updated_at,
         status_id
    FROM animal_friends
   UNION
  SELECT id,
         animal_friend_id AS animal_id,
         animal_id AS animal_friend_id,
         created_at,
         updated_at,
         status_id
    FROM animal_friends
)

If you dont want to have double entries for friends with relations both ways you could do this:

CREATE VIEW unique_animal_friends (
  SELECT MIN(id), animal_id, animal_friend_id, MIN(created_at), MAX(updated_at), MIN(status_id)
    FROM
   (SELECT id,
           animal_id,
           animal_friend_id,
           created_at,
           updated_at,
           status_id
      FROM animal_friends
     UNION
    SELECT id,
           animal_friend_id AS animal_id,
           animal_id AS animal_friend_id,
           created_at,
           updated_at,
           status_id
      FROM animal_friends) AS all_animal_friends
  GROUP BY animal_id, animal_friend_id
)

You would need a way to decide which status_id to use in case there are two conflicting ones. I chose MIN(status_id) but that is probably not what you want.

In Rails you can do this now:

class Animal < ActiveRecord::Base
  has_many :unique_animal_friends
  has_many :friends, :through => :unique_animal_friends
end

class UniqueAnimalFriend < ActiveRecord::Base
  belongs_to :animal
  belongs_to :friend, :class_name => "Animal"
end

This is out of my head and not tested. Also, you might need some plugin for view handling in rails (like "redhillonrails-core").

OTHER TIPS

There is a plugin that does what you want.

There is a post about it here.

There is an alternative here.

Both allow you do to joining conditions and are just using lazy initialization. So you can use dynamic conditions. I find the former prettier, but you can use the latter if you don't want to install plugins.

The two ways to create many to many relationships in active record are has_and_belongs_to_many and has_many :through. This site critiques the differences between the two. You don't have to write any SQL using these methods.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top