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


Morgan G. - Just Landed
General Tel: +34 91 590 2611
[email protected]

http://www.justlanded.com - Helping people abroad!
30 countries, in up to 8 languages, more to come…

2008/6/11 Frederick C. [email protected]:

user_id, :final => true
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.

Morgan G. - Just Landed

Morgan G. - Just Landed
General Tel: +34 91 590 2611
[email protected]

http://www.justlanded.com - Helping people abroad!
30 countries, in up to 8 languages, more to come…

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.

Morgan G. - Just Landed
General Tel: +34 91 590 2611
[email protected]

http://www.justlanded.com - Helping people abroad!
30 countries, in up to 8 languages, more to come…

2008/6/11 Morgan G. [email protected]:

Thanks for that.

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.

Cheers.

Morgan G. - Just Landed
General Tel: +34 91 590 2611
[email protected]

http://www.justlanded.com - Helping people abroad!
30 countries, in up to 8 languages, more to come…

On 11 Jun 2008, at 15:07, Morgan G. wrote:

  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

2008/6/11 Morgan G. [email protected]:

That sounds eminently plausible. with_exclusive_scope might help you
UserFriend.create :final => true unless UserFriend.exists?
hear it.
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.

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.

Morgan G. - Just Landed
General Tel: +34 91 590 2611
[email protected]

http://www.justlanded.com - Helping people abroad!
30 countries, in up to 8 languages, more to come…

2008/6/11 Frederick C. [email protected]:

unknown keys error.
end

You’re right. Works fine.

Cheers.

Morgan G. - Just Landed
General Tel: +34 91 590 2611
[email protected]

http://www.justlanded.com - Helping people abroad!
30 countries, in up to 8 languages, more to come…

On 11 Jun 2008, at 15:44, Morgan G. wrote:

actions and params, not params directly.

I’ll check this out now.

conceivably you could just do
with_exclusive_scope do

end

Fred