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.
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, adrinking 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.
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.
Which gives the following migration:
Run in the console:
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.
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.
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.
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:
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:
Operator | is an operator of combining arrays. To me, it’s so very beautiful.
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:
Add the following to the User model:
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:
Now do the rake:
Launch rails console
We try to perform:
PS:
Applied programmers respect and good luck from the school teacher!
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 (
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
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!