Clam
Clam

Reputation: 945

Creating Relationships in Neo4J model with after_save

So I apologize for how noobish these questions may seem. I'm new to rails and as a first task I also brought in Neo4J as it seemed like the best fit if I grow the project.

I'll explain the flow of actions then show some example code. I'm trying to add in step 3-5 now.

  1. User logs in via FB
  2. The first login creates a user node. If the user exist, it simply retrieves that user+node
  3. After the user node is created, the koala gem is used to access the FB Graph API
  4. Retrieves friendlist of each friend using the app.
  5. Go through each friend and add a two way friendship relationship between the two users

As 3-5 only needs to happen when the user first joins, I thought I could do this in a method associated with after_save callback. There is a flaw to this logic though as I will need to update the user at some point with additional attributes and it will call after_save again. Can I prevent this from occurring with update?

SessionsController for reference

  def create
    user = User.from_omniauth(env["omniauth.auth"])
    session[:user_id] = user.id  
    redirect_to root_url
  end

  def destroy
    session.delete(:user_id)
    redirect_to root_path
  end

So in my user.rb I have something like this

 has_many :both, :friendships

  after_save :check_friends


  def self.from_omniauth(auth)
    @user = User.where(auth.slice(:provider, :uid)).first

    unless @user
      @user = User.new
      # assign a bunch of attributes to @user

      @user.save!
    end
    return @user
  end

  def facebook
    @facebook ||= Koala::Facebook::API.new(oauth_token)

    block_given? ? yield(@facebook) : @facebook
      rescue Koala::Facebook::APIError => e
      logger.info e.to_s
      nil
  end

  def friends_count
    facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
  end

  def check_friends(friendships)
    facebook.get_connection("me", "friends").each do |friend|
      friend_id = friend["id"]
      friend_node = User.where(friend_id)
      Friendship.create_friendship(user,friend_node)
      return true
    end
  end

friendship.rb

  from_class User
  to_class   User
  type 'friendship'

  def self.create_friendship(user,friend_node)
    friendship = Friendship.create(from_node: user, to_node: friend_node)
  end   

I'm not sure if I'm on the right track with how to create a relationship node. As I just created @user, how do I incorporate that into my check_friends method and retrieve the user and friend node so properly so I can link the two together.

Right now it doesn't know that user and friend_user are nodes

If you see other bad code practice, please let me know!

In advance: Thanks for the help @subvertallchris. I'm sure you will be answering lots of my questions like this one.

Upvotes: 4

Views: 735

Answers (1)

subvertallchris
subvertallchris

Reputation: 5472

This is a really great question! I think that you're on the right track but there are a few things you can change.

First, you need to adjust that has_many method. Your associations always need to terminate at a node, not ActiveRel classes, so you need to rewrite that as something like this:

has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'

You'll run into some problems otherwise.

You may want to consider renaming your relationship type in the interest of Neo4j stylistic consistency. I have a lot of bad examples out there, so sorry if I gave you bad ideas. FRIENDS_WITH would be a better relationship name.

As for handling your big problem, there's a lot you can do here.

EDIT! Crap, I forgot the most important part! Ditch that after_save callback and make the load existing/create new user behavior two methods.

class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(env["omniauth.auth"])
    @user = user.nil? ? User.create_from_omniauth(env["omniauth.auth"]) : user
    session[:user_id] = @user.id
    redirect_to root_url
  end

  def destroy
    session.delete(:user_id)
    redirect_to root_path
  end
end


class User
  include Neo4j::ActiveNode
  # lots of other properties
  has_many :both, :friends, model_class: 'User', rel_class: 'Friendship'

  def self.from_omniauth(auth)
    User.where(auth.slice(:provider, :uid)).limit(1).first
  end

  def self.create_from_omniauth(auth)
    user = User.new
    # assign a bunch of attributes to user
    if user.save!
      user.check_friends
    else
      # raise an error -- your user was neither found nor created
    end
    user
  end

  # more stuff
end

That'll solve your problem with getting it started. You may want to wrap the whole thing in a transaction, so read about that in the wiki.

But we're not done. Let's look at your original check_friends:

def check_friends(friendships)
  facebook.get_connection("me", "friends").each do |friend|
    friend_id = friend["id"]
    friend_node = User.where(friend_id)
    Friendship.create_friendship(user,friend_node)
    return true
  end
end

You're not actually passing it an argument, so get rid of that. Also, if you know you're only looking for a single node, use find_by. I'm going to assume there's a facebook_id property on each user.

def check_friends
  facebook.get_connection("me", "friends").each do |friend|
    friend_node = User.find_by(facebook_id: friend["id"])
    Friendship.create_friendship(user,friend_node) unless friend_node.blank?
  end
end

The create_friendship method should should return true or false, so just make that the last statement of the method does that and you can return whatever it returns. That's as easy as this:

def self.create_friendship(user, friend_node)
  Friendship.new(from_node: user, to_node: friend_node).save
end

create does not return true or false, it returns the resultant object, so chaining save to your new object will get you what you want. You don't need to set a variable there unless you plan on using it more within the method.

At this point, you can easily add an after_create callback to your ActiveRel model that will do something on from_node, which is always the User you just created. You can update the user's properties however you need to from there. Controlling this sort of behavior is exactly why ActiveRel exists.

I'd probably rework it a bit more, still. Start by moving your facebook stuff into a module. It'll keep your User model cleaner and more focused.

# models/concerns/facebook.rb

module Facebook
  extend ActiveSupport::Concern

  def facebook
    @facebook ||= Koala::Facebook::API.new(oauth_token)

    block_given? ? yield(@facebook) : @facebook
      rescue Koala::Facebook::APIError => e
      logger.info e.to_s
      nil
  end

  def friends_count
    facebook { |fb| fb.get_connection("me", "friends", summary: {}) }
  end
end

# now back in User...

class User
  include Neo4j::ActiveNode
  include Facebook
  # more code...
end

It's really easy for your models to become these messy grab bags. A lot of blogs will encourage this. Fight the urge!

This should be a good start. Let me know if you have any questions or if I screwed anything up, there's a lot of code and it's possible I may need to clarify or tweak some of it. Hope it helps, though.

Upvotes: 5

Related Questions