Reflections on the implementation of the social graph

    Hello!

    We are all used to using social networks. One of their foundations is the establishment of socially significant relationships between users. Typically, these connections are friendships or fans (followers).

    I don’t know what came up against me, but after returning from school (I work as a teacher) I decided to try to create something on my favorite rails that my opinion could help me implement the functionality of the social graph on the school website. And I decided not to be limited to two types of connections.

    Let's try to fantasize about a social graph and write some Rails code.



    Some time ago, I had to deal with the implementation of the functionality of social connections in ROR projects several times. In the first case, it was a project in which friendship between the participants was realized, in the second, “follower” connections were created. Both projects are commercial - I do not name them. Sorry.

    The general point was that a connection was created with a name similar to Friendship in which there were 2 user identifiers and the status of this connection. pending - an application for friendship has been submitted and is awaiting confirmation, accepted - an application has been confirmed and active, rejected - an application has been rejected, deleted - a connection has been deleted.

    In addition, I noticed that usually when creating a connection from one person No. 1 to person No. 2 (in those implementations that I saw) a second twin connection is created , which differs only in that the user IDs are rearranged. The status of the twin recording is copied from the original with every change. This approach is understandable - the selection of links for a particular user is carried out by a single query to the database. However, you need an additional record in the database and control over the change in the status of the record.

    Looking ahead, I’ll say that in my version of the code I decided not to produce records and went along the path of submitting 2 queries to the database.

    The world is more complicated than it is displayed on social networks



    Large projects do not provide a large number of links. Why? I dont know. Perhaps the human psyche is not yet ready for this, but ... In a private conversation with one of my former teachers from a local university, the thought slipped through: communication between people is more complicated than is presented on the networks . There are teachers and students, superiors and subordinates, officers and their soldiers, site administrators, and users, parents and children, etc., etc., etc.

    Have you noticed? Often the roles of people in the relationship are not equivalent, it is not easy for you - friends. Everything is a bit more complicated.

    Also, as a rule, a social connection has a context (life, work, army, school) - i.e. the place where this connection was established.

    What do I dislike about social networks now? So it’s that when you add random acquaintances or people you only know in person and then remove them from your “track record” during the audit ( bad mood ), sometimes you have to explain - they say, I'm sorry, you're not an enemy to me, and personally I don’t have anything against you - but I don’t want to keep you in the list of “friends” anymore - we have not seen each other for several years ( and I don’t even remember your name ) - sorry, but I don’t see much point.

    This I lead to what would be great if in the social. different options were provided for in the networks - a friend, a sports coach, my grandmother, a classmate from school, a drinking companion , a colleague from work, a boss, a department head, etc.

    After spending 45 minutes of Rails 3 time, I tried to throw a prototype of something that suddenly excited my sore teacher’s mind.

    Model



    The model (I will call it Graph) contains 2 user IDs (the applicant and the recipient of the application), the status of the application, the role of the sender and the role of the recipient, as well as the context of social communication.
    rails g model graph context:string sender_id:integer sender_role:string recipient_id:integer recipient_role:string state:string 


    Which gives the following migration:
    
    class CreateGraphs < ActiveRecord::Migration
      def self.up
        create_table :graphs do |t|
          t.string :context
          t.integer :sender_id
          t.string :sender_role
          t.integer :recipient_id
          t.string :recipient_role
          t.string :state
          t.timestamps
        end
      end
      def self.down
        drop_table :graphs
      end
    end
    


    Run in the console:
    rake db:migrate

    Which will create the necessary table with the specified fields in the database.

    In the Graph model file itself, using the state machine, I determined which states a graph element can accept, and scope will allow me to supplement the database queries with the necessary conditions.

    
    class Graph < ActiveRecord::Base
      scope :pending, where(:state => :pending)
      scope :accepted, where(:state => :accepted)
      scope :rejected, where(:state => :rejected)
      scope :deleted, where(:state => :deleted)
      #state pending, accepted, rejected, deleted
      state_machine :state, :initial => :pending do
        event :accept do
          transition :pending => :accepted
        end
        event :reject do
          transition :pending => :rejected
        end
        event :delete do
          transition all => :deleted
        end
        event :initial do
          transition all => :pending
        end
      end
    end
    


    To the User model (it is in every Rails App in any way) I will first add a method: graph_to , which will return me a graph element to this user (if a graph element exists) or just create a new element.

    I build a graph element from the current user to another user, in some context, where I am someone and the recipient is also someone (according to predefined roles).
    By default, the context is life, and users have roles - a friend.

    
    class User < ActiveRecord::Base
      def graph_to(another_user, opts={:context=>:live, :me_as=>:friend, :him_as=>:friend})
        Graph.where(:context=>opts[:context], :sender_id=>self.id, :sender_role=>opts[:me_as], :recipient_id=>another_user, :recipient_role=>[:him_as]).first ||
        graphs.new( :context=>opts[:context], :sender_role=>opts[:me_as], :recipient_id=>another_user.id, :recipient_role=>opts[:him_as])
      end
    end
    


    Experiments will require many records of user relationships. Therefore, I created a rake, which from the console allows me to create several dozen users and establish random connections between them.

    I explain for those who can not read in rubles.
    • Users are created in the code.
    • Relationship contexts are established.
    • For each context, user roles are set.
    • Applications are randomly submitted and, in the same way, applications randomly receive a status

    
    namespace :db do
      namespace :graphs do
        # rake db:graphs:create
        desc 'create graphs for development'
        task :create => :environment do
          i = 1
          puts 'Test users creating'
          100.times do |i|
            u = User.new(
              :login => "user#{i}",
              :email => "test-user#{i}@ya.ru",
              :name=>"User Number #{i}",
              :password=>'qwerty',
              :password_confirmation=>'qwerty'
            )
            u.save
            puts "test user #{i} created"
            i = i.next
          end#n.times
          puts 'Test users created'
          contexts = [:live, :web, :school, :job, :military, :family]
          roles={
            :live=>[:friend,:friend],
            :web=>[:moderator, :user],
            :school=>[:teacher, :student],
            :job=>[:chief, :worker],
            :military=>[:officer, :soldier],
            :family=>[:child, :parent]
          }
           users = User.where("id > 10 and id < 80") #70 users
           test_count = 4000
           test_count.times do |i|
              sender = users[rand(69)]
              recipient = users[rand(69)]
              context = contexts.rand # :job
              role = roles[context].shuffle # [:worker, :chiеf]
              # trace
              p "test graph #{i}/#{test_count} " + sender.class.to_s+" to "+recipient.class.to_s + " with context: " + context.to_s
              graph = sender.graph_to(recipient, :context=>context, :me_as=>role.first, :him_as=>role.last)
              graph.save
              # set graph state
              reaction = [:accept, :reject, :delete, :initial].rand
              graph.send(reaction)
           end# n.times
        end# db:graphs:create
      end#:graphs
    end#:db
    


    Inverted Graph Elements


    Since I said earlier that I did not want to create a twin-link social link this time, I will have to perceive each link in both forward and reverse directions.

    I will do this by adding the lines to the User model:
    
      has_many :graphs, :foreign_key=>:sender_id
      has_many :inverted_graphs, :class_name => 'Graph', :foreign_key=>:recipient_id
    

    Each user has many direct links (where he is the initiator of the link), and reverse, where he is the recipient of a request for social link. These elements differ only in different foreign keys.

    To select all the social connections of this user, I will have to select all of his direct and feedback connections, and then combine the arrays of records. For example, to select all the added bosses from my work, you would write something like the following:
    
    def accepted_chiefs_from_job
       chiefs = graphs.accepted.where(:context => :job, :recipient_role=>:chief) # my graphs
       _chiefs = inverted_graphs.accepted.where(:context => :job, :sender_role=>:chief) # foreign graphs
      chiefs | _chiefs
     end
    

    Operator | is an operator of combining arrays. To me, it’s so very beautiful.

    A bit of meta programming and ruby ​​magic


    I have a lot of contexts and user roles in relationships. I need a lot of methods like the above accepted_chiefs_from_job method which selects all my superiors from the job that I agreed to add. You do not think to write them manually?
    We use meta-programming so that Ruby himself creates the necessary methods for us and makes the appropriate selections. The magic method method_missing (method_name, * args) will help in this . This method is called when Ruby does not find any method. Here we will explain to him what needs to be done if he meets an attempt to select data from the graph.

    Ruby will create methods like these:
    
    user.accepted_friends_from_live
    user.rejected_friends_from_live
    user.deleted_friends_from_live
    user.deleted_chiefs_from_job
    user.accepted_chiefs_from_job
    user.rejected_chiefs_from_job
    user.accepted_teachers_from_school
    user.deleted_teachers_from_school
    


    Add the following to the User model:
    
      def method_missing(method_name, *args)
        if /^(.*)_(.*)_from_(.*)$/.match(method_name.to_s)
          match = $~
          state = match[1].to_sym
          role = match[2].singularize.to_sym
          context = match[3].to_sym
          graphs.send(state).where(:context => context, :recipient_role=>role) | inverted_graphs.send(state).where(:context => context, :sender_role=>role)
        else
          super
        end
      end
    


    If method_missing (method_name, * args) does not find any method, then it will try to parse it regularly. If the regularity fits the name of the methods of our graph, then the cut itself will compile a request based on the data received from the line and return the result. If the called method does not fit the regular schedule , then method_missing (method_name, * args) will simply go over to its standard behavior - super , and will probably give a code execution error.

    User Summary Code:
    
    class User < ActiveRecord::Base
      has_many :pages
      has_many :graphs, :foreign_key=>:sender_id
      has_many :inverted_graphs, :class_name => 'Graph', :foreign_key=>:recipient_id
      def method_missing(method_name, *args)
        if /^(.*)_(.*)_from_(.*)$/.match(method_name.to_s)
          match = $~
          state = match[1].to_sym
          role = match[2].singularize.to_sym
          context = match[3].to_sym
          graphs.send(state).where(:context => context, :recipient_role=>role) | inverted_graphs.send(state).where(:context => context, :sender_role=>role)
        else
          super
        end
      end
      def graph_to(another_user, opts={:context=>:live, :me_as=>:friend, :him_as=>:friend})
        Graph.where(:context=>opts[:context], :sender_id=>self.id, :sender_role=>opts[:me_as], :recipient_id=>another_user, :recipient_role=>[:him_as]).first ||
        graphs.new( :context=>opts[:context], :sender_role=>opts[:me_as], :recipient_id=>another_user.id, :recipient_role=>opts[:him_as])
      end
    end
    

    That's it


    Now do the rake:
    rake db:graphs:create

    Launch rails console
    rails c

    We try to perform:
    
    u = User.find(10)
    u.graph_to(User.first, :context=>:job, :me_as=>:boss, :him_as=>:staff_member)
    u.graph_to(User.last, :context=>:school, :me_as=>:student, :him_as=>:teacher)
    u.graph_to(User.find(20), :context=>:school, :me_as=>:student, :him_as=>:school)
    u.accepted_friends_from_live
    u.rejected_friends_from_live
    u.deleted_friends_from_live
    u.deleted_chiefs_from_job
    u.accepted_chiefs_from_job
    u.rejected_chiefs_from_job
    


    PS:
    Applied programmers respect and good luck from the school teacher!

    Also popular now: