after_save filter on the join model cannot create new objects

Establishing a friend relationship with another user with a join model (user_id, friend_id). Normally this would result in one-way relationships (Jack is a friend of Jill but Jill is not a friend of Jack). To get around this I want to have an inverted entry in the join model table (not the nicest approach but I see no reason why it cannot work) so I set up my models like this:

class User < ActiveRecord::Base
has_many :user_friends, :dependent => :destroy
has_many :friends, :through => :user_friends, :order => :first_name
end

class UserFriend < ActiveRecord::Base
attr_accessor :final

belongs_to :user
belongs_to :friend, :foreign_key => :friend_id, :class_name => “User”
validates_presence_of :user, :friend

Make the relationship bidirectional

after_save :duplicate_inverted
after_destroy :destroy_inverted

protected

# Create an inverted version of this record (unless one already exists)
def duplicate_inverted
  return true if self.final
  unless UserFriend.exists? :user_id => friend_id, :friend_id => user_id
    self.class.create :user_id => friend_id, :friend_id => user_id, :final => true
  end
end

# Remove the inverted version of this object
def destroy_inverted
  if UserFriend.exists? :user_id => friend_id, :friend_id => user_id
    self.class.destroy :user_id => friend_id, :friend_id => user_id
  end
end

end

The UserFriend#final method exists so the iteration only happens once.

What happens, though, when I run this:

u1 = User.find 1
u2 = User.find 2
u1.friends.clear
u1.friends << u2

User Columns (0.001914) SHOW FIELDS FROM users
User Load (0.000986) SELECT * FROM users WHERE (users.id = 1)
User Load (0.000980) SELECT * FROM users WHERE (users.id = 2)
User Load (0.001556) SELECT users.* FROM users INNER JOIN user_friends ON users.id = user_friends.friend_id WHERE ((user_friends.user_id = 1)) ORDER BY first_name
SQL (0.000288) BEGIN
UserFriend Columns (0.000853) SHOW FIELDS FROM user_friends
User Load (0.000990) SELECT * FROM users WHERE (users.id = 1)
User Load (0.000960) SELECT * FROM users WHERE (users.id = 2)
*** UserFriend Create (0.000443) INSERT INTO user_friends (updated_at, user_id, friend_id, created_at) VALUES(‘2008-06-11 15:04:46’, 1, 2, ‘2008-06-11 15:04:46’)
UserFriend Exists (0.000477) SELECT user_friends.id FROM user_friends WHERE (user_friends.user_id = 2 AND user_friends.friend_id = 1) LIMIT 1
User Load (0.000993) SELECT * FROM users WHERE (users.id = 1)
User Load (0.000967) SELECT * FROM users WHERE (users.id = 2)
*** UserFriend Create (0.000396) INSERT INTO user_friends (updated_at, user_id, friend_id, created_at) VALUES(‘2008-06-11 15:04:46’, 1, 2, ‘2008-06-11 15:04:46’)
SQL (0.020969) COMMIT

I’ve put *** in front of the two important rows. The first creates the requested object, the second creates the inverted one. However, the second insert has user_id and friend_id set to the same values as the first insertion?

I tested this by replacing
unless UserFriend.exists? :user_id => friend_id, :friend_id => user_id
self.class.create :user_id => friend_id, :friend_id => user_id, :final => true
end

with
unless UserFriend.exists? :user_id => friend_id, :friend_id => user_id
self.class.create :user_id => 5, :friend_id => 5, :final => true
end

and the results were exactly the same. This leads me to believe there is some sort of with_scope or something going one which is appending those values to my create call but I don’t know how to find out whether or not this is the case.

Can anyone shine some light on this behaviour?

Cheers,
Morgan Grubb.

I tested this by replacing
      unless UserFriend.exists? :user_id => friend_id, :friend_id =>
user_id
        self.class.create :user_id => friend_id, :friend_id =>
user_id, :final => true
      end

with
      unless UserFriend.exists? :user_id => friend_id, :friend_id =>
user_id
        self.class.create :user_id => 5, :friend_id => 5, :final =>
true
      end

and the results were exactly the same. This leads me to believe
there is some sort of with_scope or something going one which is
appending those values to my create call but I don't know how to
find out whether or not this is the case.

That sounds eminently plausible. with_exclusive_scope might help you
out.

Fred

I tested this by replacing

  unless UserFriend.exists? :user_id => friend_id, :friend_id =>

user_id

    self.class.create :user_id => friend_id, :friend_id =>

user_id, :final => true

  end

with

  unless UserFriend.exists? :user_id => friend_id, :friend_id =>

user_id

    self.class.create :user_id => 5, :friend_id => 5, :final =>

true

  end

and the results were exactly the same. This leads me to believe

there is some sort of with_scope or something going one which is

appending those values to my create call but I don’t know how to

find out whether or not this is the case.

That sounds eminently plausible. with_exclusive_scope might help you

out.

Thanks for that.

I’ve been trying a few variants and running into issues. It seems to me this

  UserFriend.send :with_exclusive_scope, { :user_id => friend_id, :friend_id => user_id } do

    UserFriend.create :final => true unless UserFriend.exists?
  end

should work but it leads to

ArgumentError: Unknown key(s): user_id, friend_id

I can see why they decided to make with_scope a protected method. It’s a real pain in the butt.

I’ll keep trying but if you can see can what I’m doing wrong I’d love to hear it.

Cheers.

and the results were exactly the same. This leads me to believe

there is some sort of with_scope or something going one which is

appending those values to my create call but I don’t know how to

find out whether or not this is the case.

That sounds eminently plausible. with_exclusive_scope might help you

out.

Thanks for that.

I’ve been trying a few variants and running into issues. It seems to me this

  UserFriend.send :with_exclusive_scope, { :user_id => friend_id, :friend_id => user_id } do


    UserFriend.create :final => true unless UserFriend.exists?
  end

should work but it leads to

ArgumentError: Unknown key(s): user_id, friend_id

I can see why they decided to make with_scope a protected method. It’s a real pain in the butt.

I’ll keep trying but if you can see can what I’m doing wrong I’d love to hear it.

Sorry, the error that specific version leads to is

TypeError: can’t dup Fixnum

Which is when I replaced friend.id with friend_id and got the unknown keys error.

Cheers.

I’m on fire today. Just realised that with_scope takes a hash of actions and params, not params directly.

I’ll check this out now.

Cheers.

Bang on. The final working bi-directional friendship version:

class UserFriend < ActiveRecord::Base
attr_accessor :final

belongs_to :user
belongs_to :friend, :foreign_key => :friend_id, :class_name => “User”
validates_presence_of :user, :friend

Make the relationship bidirectional

after_save :duplicate_inverted
after_destroy :destroy_inverted

protected

# Create an inverted version of this record (unless one already exists)

def duplicate_inverted
  return true if self.final
  UserFriend.send(:with_exclusive_scope, :find => {}, :create => {}) do
    unless UserFriend.exists? :user_id => friend_id, :friend_id => user_id

      UserFriend.create :final => true, :user_id => friend_id, :friend_id => user_id
    end
  end
end

# Remove the inverted version of this object
def destroy_inverted

  if UserFriend.exists? :user_id => self.friend_id, :friend_id => self.user_id
    UserFriend.destroy_all :user_id => self.friend_id, :friend_id => self.user_id
  end
end

end

Thanks for your help.

Sorry, the error that specific version leads to is

TypeError: can't dup Fixnum

Which is when I replaced friend.id with friend_id and got the
unknown keys error.

I'm on fire today. Just realised that with_scope takes a hash of
actions and params, not params directly.

I'll check this out now.

conceivably you could just do
with_exclusive_scope do
...
end

Fred

You’re right. Works fine.

Cheers.