2

What would be the proper way to create a relationship between Users that have a bidirectional relationship?

Say I have:

class User
  include Neo4j::ActiveNode

  property :name,   type: String
  property :created_at, type: DateTime
  property :updated_at, type: DateTime

  has_many :both, :friends, model_class: 'User', type: 'connection' unique: true
  has_many :in, :followers, model_class: 'User', type: 'connection', unique: true
  has_many :out, :following, model_class: 'User', type: 'connection', unique: true
end

And then,

users = [
  User.create(name: 'Foo'),
  User.create(name: 'Bar'),
  User.create(name: 'Baz'),
]

Would this be the appropriate way to do it? It seems wildly inefficient:

users.each do |user|
  user.friends << users.reject { |u| u == user }
end
bswinnerton
  • 4,533
  • 8
  • 41
  • 56
  • 1
    I'm confused, why are you iterating over `users` twice? Also why are you using `<<` to add an array? I think that's only for individual nodes. You could build an array and then do something like `user.friends = user_array` – Brian Underwood Apr 22 '15 at 05:24
  • @BrianUnderwood, that was a poor attempt at ensuring that the user does not have a relationship to themselves. Is there a better way you would suggest accomplishing that? – bswinnerton Apr 22 '15 at 13:45
  • Oh, I see, I you're making a relationship between every pair – Brian Underwood Apr 22 '15 at 20:02

1 Answers1

2

If you want to ensure that user.friends returns the other two users, I'd actually do this:

users.each do |user|
  other_users = users.reject { |u| u == user || user.friends.include?(u) }
  user.friends << other_users
end

Now that is wildly inefficient! It creates a new array and performs an additional lookup against the database for each user. (FWIW, the include? lookup is very fast. You can also remove that reject and iterate through users once again to speed it up.) Still, it's necessary to accomplish this because otherwise, you'll create duplicate relationships.

Setting unique: true in your association is only going to save you in one direction. Queries against has_many :both associations use a direction-agnostic Cypher MATCH but CREATE actions always need direction, so they go from the node on the left to the node on the right. user1.friends << user2 and user2.friends << user1 will create two relationships between the two nodes, one in each direction.

EDIT

As you pointed out in your own comment to an issue on Neo4j.rb, using user.friends = [other_friends] will always clear out all existing relationships and prevent duplicates. It's a really cool way of solving the problem.

subvertallchris
  • 5,282
  • 2
  • 25
  • 43
  • Much thanks! Is there a substantial cost to having duplicate relationships? It seems counterintuitive to create a "follower" relationship when really the logic is such that the user must be "followed" _and_ "following" in order to be "friends". – bswinnerton Apr 22 '15 at 13:44
  • That isn't true, though. `user.friends` omits direction: `MATCH (u)-[r:connection]-(other)`. It will match all connections over that relationship type, regardless of direction. Two users will show as "friends" if there is a relationship present. – subvertallchris Apr 22 '15 at 14:04