1
votes

I have relationship between User models defined through Friendship model. (ROR 4)

User

class User < ActiveRecord::Base
  has_many :friendships, ->(object) { where('user_id = :id OR friend_id = :id', id: object.id) }

  has_many :friends, ->(object) { where(friendships: {status: 'accepted'}).where('user_id = :id OR friend_id = :id', id: object.id) }, through: :friendships, source: :friend

  has_many :requested_friends, -> { where(friendships: {status: 'pending'}) }, through: :friendships, source: :friend
end

Friendship

class Friendship < ActiveRecord::Base
  belongs_to :user
  belongs_to :friend, class_name: 'User'

  def self.request(user, friend)
    unless user == friend or find_friendship(user, friend) != nil
      create(user: user, friend: friend, status: 'pending')
    end
  end

  def self.find_friendship(user, friend)
    ids = [user.id, friend.id]
    where(user_id: ids, friend_id: ids).first
  end
end

However, this does not work and my tests are failing because of SQL queries produced.

Friendships relation

> user.friendships

Query:

SELECT "friendships".* FROM "friendships" 
    WHERE "friendships"."user_id" = ? 
      AND (user_id = 1 OR friend_id = 1)  [["user_id", 1]]

So part of WHERE before AND "kills" my actual where. I made a workaround by making instance method:

def friendships
  self.class
    .select('friendships.* FROM `friendships`')
    .where('user_id = :id OR friend_id = :id', id)
end

Is there a way I can remove my instance method and modify has_many relation to produce the SQL I want?

Requested_friends relation

> Friendship.request(user, friend)
> friend.requested_friends

Query:

SELECT "users".* FROM "users" 
    INNER JOIN "friendships" ON "users"."id" = "friendships"."friend_id" 
    WHERE "friendships"."status" = 'pending' 
      AND "friendships"."user_id" = ? 
      AND (user_id = 2 OR friend_id = 2)  [["user_id", 2]]

It obviously isn't what I need so I made a workaround by removing has_many :requested_friends and making an instance method:

def requested_friends
  self.class
    .joins('JOIN `friendships` friendships ON users.id = friendships.user_id')
    .where('friendships.status = ?', 'pending')
    .where('friendships.friend_id = ?', id)
end

Is there any way I can modify my has_many :requested_friends relation to produce same SQL as my instance method?

2
What do the tests that are failing look like? - Pavling

2 Answers

0
votes

Very confusing - I'd do something like this:

#app/models/user.rb
Class User < ActiveRecord::Base
    has_many :friendships, class_name: "user_friendships", association_foreign_key: "user_id", foreign_key: "friend_id", 
    has_many :friends, class_name: "User", through: :friendships
end

#app/models/user_friendship.rb
Class UserFriendship < ActiveRecord::Base
    belongs_to :user
    belongs_to :friend, class_name: "User"
end 

You'd have a join table which looks like this:

user_friendships
id | user_id | friend_id | other | info | created_at | updated_at

This should work (I'm not sure about the self referential association). If it does, it will allow you to call:

@user.friends

I hope this helps?

You might also benefit from this gem

0
votes

you cannot achieve the SQL you want using has_many method with condition. The reason is that the block you pass to the method is only additional condition, on top of the standard query which checks if user_id = ?.

Instead you can simplify your instance method a little bit

def friendships Friendship.where('user_id = :id or friend_id = :id', id) end