📜 ⬆️ ⬇️

Reflections on the implementation of the social graph

Hello!

We are all accustomed to using social networks. One of their foundations is the establishment of socially significant links between users. As a rule, these connections are friendship or fans (followers).

I don’t know what I’ve found on me, but after returning from school (I work as a teacher) I decided to try to create something on my favorite rails that could help me to realize the functionality of a social graph on the school website. And I decided not to limit myself to two types of connections.
')
Let's try to fantasize about the social graph and write some Rails code.



Some time ago I had to deal with the implementation of social connections in ROR projects several times. In the first case, it was a project in which friendship was realized between the participants, in the second they created follower-type communications. Both projects are commercial - I don’t name them. Excuse me.

The general essence was that a connection was created with a name similar to Friendship in which there were 2 user identifiers and the state of this connection. pending - a friend request has been submitted and is awaiting confirmation, accepted - the request is confirmed and active, rejected - the application is rejected, deleted - the 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 I saw) , a second twin connection is created , which differs only in that the users id are swapped. The status of the twin is copied from the original with each change. This approach is understandable - the selection of links for a specific user is carried out by a single query to the database. However, you need an additional entry in the database and ensuring control of the change in the status of the entry.

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

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



In large projects there is not 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: connections between people are more complicated than it is represented in networks . There are teachers and students, chiefs and subordinates, officers and their soldiers, site administrators, and users, parents and children, and so on, and so forth.

Did you notice? Often the roles of people in the relationship are not equal, it is not easy for you - friends. Everything is a little more complicated.

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

What I don't like on social networks now? So this is what adding friends as casual acquaintances or people you only know in person, and then removing them from your “track record” during an audit ( bad mood ) sometimes has to be explained - they say I'm sorry, you are not the enemy to me and I personally have nothing against you - but I don’t want to keep you on “friends” anymore - we haven’t 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 the fact that it would be great if in the social. Networks have been provided for different options - an acquaintance, a sports trainer, my grandmother, a classmate from school, a drinking companion , a colleague from work, a chef, a head of a department, etc.

Spending 45 minutes on Rails 3, I tried to build up a prototype of what unexpectedly agitated my sore teacher mind.

Model



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


What 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 console:
 rake db:migrate 

That will create for us in a DB the necessary table with the set fields.

In the Graph model file itself, I determined by the state machine what states the graph element can accept, and the 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 


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

The graph element I build 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 a lot of user relationship records. 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 ruby.

 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, :chif] # 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 didn’t want to create a twin record for each social link this time, I’ll have to take every link both in the forward and in the opposite direction.

I will do this by adding 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 the request for a social link. These elements differ only in different foreign keys.

In order to select all the social links of a given user, I will have to select all of his direct and feedback links, and then combine the arrays of records. For example, to select all the added superiors from my work, you need to 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 to combine arrays. For me, so very beautiful.

Some meta programming and ruby ​​magic


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

Ruby himself 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 on a regular basis. If the regulars match the name of the methods of our graph, then Ruby will compose the query on the data obtained from the string and return the result. If the callable method does not fit the regular schedule , then method_missing (method_name, * args) will simply go to its standard behavior, super , and will probably give a code execution error.

Summary User 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 

Well that's all


Now perform the rake:
 rake db:graphs:create 

Run 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 wish good luck from the school teacher!

Source: https://habr.com/ru/post/112221/


All Articles