Здравствуйте!
Мы все привыкли пользоваться социальными сетями. Одной из их основ является установление социально значимых связей между пользователями. Как правило, эти связи — дружба или поклонники (последователи).
Не знаю что на меня нашло, но вернувшись из школы (я работаю учителем) решил попробовать создать на любимых рельсах что-то, что нам мой взгляд могло бы помочь мне реализовать функционал социального графа на школьном сайте. И двумя типами связей я решил не ограничиваться.
Попробуем пофантазировать на тему социального графа и написать немного Rails кода.
Некоторое время назад мне пришлось несколько раз сталкиваться с реализацией функционала социальных связей в ROR проектах. В первом случае это был проект в котором реализовывалась дружба между участниками, во втором создавались связи типа «последователь». Оба проекта коммерческие — имен не называю. Извините.
Общая суть была в том, что создавалась связь с названием похожим на Friendship в которой было 2 идентификатора пользователей и состояние этой связи. pending — заявка на дружбу подана и ожидает подтверждения, accepted — заявка подтверждена и активна, rejected — заявка отклонена, deleted — связь удалена.
Кроме того, я обратил внимание, что обычно при создании связи от одного человека №1 к человеку №2 (в тех реализациях которые я видел) создается вторая связь-близнец, которая отличается только тем, что id пользователей переставлены местами. Состояние записи-близнеца копируются из оригинала при каждом изменении. Такой подход понятен — выборки связей для конкретного пользователя проводятся одним запросом к БД. Однако, вам необходимо дополнительная запись в БД и обеспечение контроля изменения статуса записи.
Забегая вперед, скажу, что я решил в своем варианте кода не плодить записи и пошел по пути подачи 2 запросов к БД.
В крупных проектах не предусмотрено большого количества связей. Почему? Я не знаю. Возможно человеческая психика еще не готова к этому, но… В частном разговоре с одним моим бывшим преподавателем из местного университета проскользнула мысль: связи между людьми сложнее, чем это представлено в сетях. Есть учителя и ученики, начальники и подчиненные, офицеры и их солдаты, администраторы сайтов, и пользователи, родители и дети и прочее, прочее, прочее.
Заметили? Часто роли людей во взаимосвязи не равнозначны, это вам не просто — друзья. Все немного сложнее.
Так же, как правило у социальной связи есть контекст (жизнь, работа, армия, школа) — т.е. место, где эта связь была установлена.
Что мне не нравится в социальных сетях сейчас? Так это то, что добавляя в друзья случайных знакомых или людей, которых ты знаешь только в лицо, а потом удаляя их из своего «послужного списка» во время ревизии (плохого настроения) порою приходится объясняться — мол, извини, ты не враг мне, и лично против тебя я ничего не имею — но держать в листе «друзей» тебя больше не хочу — мы не виделись уже несколько лет (и я даже не помню как тебя зовут) — sorry, но не вижу особого смысла.
Это я веду к тому, что было бы здорово, если бы в соц. сетях были предусмотрены разные варианты — знакомый, спортивный тренер, моя бабушка, одноклассник из школы,собутыльник, коллега с работы, шеф, руководитель отдела и.т.д.
Уделив 45 минут времени Rails 3 я попробовал накидать некий прототип того, что неожиданно взбудоражило мой воспаленный учительский разум.
Модель (я назову ее Graph) содержит 2 id пользователей (подавшего заявку и получателя заявки), статус заявки, роль отправителя и роль получателя, а так же контекст социальной связи.
Что, дает следующую миграцию:
Выполним в консоли:
Что создаст нам в БД необходимую таблицу с заданными полями.
В самом файле модели Graph я с помощью state machine определил какие состояния может принимать элемент графа, а scope позволит мне дополнить запросы к БД необходимыми условиями.
В модель User (она по-любому есть в каждом Rails App) я для начала добавлю метод: graph_to, который вернет мне элемент графа к данному пользователю (если элемент графа существует) или просто создаст новый элемент.
Элемент графа я строю от текущего пользователя до другого пользователя, в некотором контексте, где я являюсь кем-то и получатель так же, является кем-то (согласно предопределенных ролей).
По-умолчанию контекстом является жизнь, а пользователи имеют роли — друг.
Для экспериментов потребуется много записей о взаимосвязях пользователей. Поэтому я создал рейк, который из консоли позволяет мне создать несколько десятков пользователей и установить между ними случайные связи.
Поясняю для тех, кто не умеет читать на руби.
Поскольку я ранее говорил, что не захотел в этот раз создавать каждой социальной связи запись-близнеца, то мне придется воспринимать каждую связь как в прямом, так и в обратном направлении.
Это я сделаю добавив в модель User строки:
Каждый пользователь имеет множество прямых связей (где он является инициатором связи), так и обратных, где он является получателем запроса на социальную связь. Эти элементы отличаются только разными внешними ключами.
Что бы выбирать все социальные связи данного пользователя мне придется выбирать все его прямые и обратные связи, а потом объединять массивы записей. Например, для выборки всех добавленных начальников с моей работы надо написать примерно следующее:
Оператор | является оператором объединения массивов. По мне, так очень красиво.
У меня очень много контекстов и ролей пользователей во взаимосвязях. Мне нужно много методов, подобных вышеизложенному методу accepted_chiefs_from_job который выбирает всех моих начальников с работы, которых я согласился добавить. Вы же не думаете писать их в ручную?
Мы используем мета-программирование, что бы руби сам создавал нам нужные методы и делал соответствующие выборки. Поможет в этом волшебный метод method_missing(method_name, *args). Этот метод вызывается когда руби не находит какой-то метод. Вот тут то мы ему и поясним, что нужно делать в случае, когда он встретит попытку выборки данных из графа.
Руби будет сам создавать методы подобные этим:
Добавим в модель User следующее:
Если method_missing(method_name, *args) не находит какой-то метод, то он попытается его распарсить по регулярке. Если регулярка подходит под название методов нашего графа, то руби сам состовит запрос по полученным из строки данным и вернет результат. Если вызываемый метод не подходит под регулярку, то method_missing(method_name, *args) просто перейдет к своему стандартном у поведению — super, и, вероятно, даст ошибку выполнения кода.
Итоговый код User:
Теперь выполняем рейк:
Запускаем rails консоль
Пробуем выполнять:
PS:
Прикладным программистам уважение и пожелание удачи от школьного учителя!
Мы все привыкли пользоваться социальными сетями. Одной из их основ является установление социально значимых связей между пользователями. Как правило, эти связи — дружба или поклонники (последователи).
Не знаю что на меня нашло, но вернувшись из школы (я работаю учителем) решил попробовать создать на любимых рельсах что-то, что нам мой взгляд могло бы помочь мне реализовать функционал социального графа на школьном сайте. И двумя типами связей я решил не ограничиваться.
Попробуем пофантазировать на тему социального графа и написать немного Rails кода.
Некоторое время назад мне пришлось несколько раз сталкиваться с реализацией функционала социальных связей в ROR проектах. В первом случае это был проект в котором реализовывалась дружба между участниками, во втором создавались связи типа «последователь». Оба проекта коммерческие — имен не называю. Извините.
Общая суть была в том, что создавалась связь с названием похожим на Friendship в которой было 2 идентификатора пользователей и состояние этой связи. pending — заявка на дружбу подана и ожидает подтверждения, accepted — заявка подтверждена и активна, rejected — заявка отклонена, deleted — связь удалена.
Кроме того, я обратил внимание, что обычно при создании связи от одного человека №1 к человеку №2 (в тех реализациях которые я видел) создается вторая связь-близнец, которая отличается только тем, что id пользователей переставлены местами. Состояние записи-близнеца копируются из оригинала при каждом изменении. Такой подход понятен — выборки связей для конкретного пользователя проводятся одним запросом к БД. Однако, вам необходимо дополнительная запись в БД и обеспечение контроля изменения статуса записи.
Забегая вперед, скажу, что я решил в своем варианте кода не плодить записи и пошел по пути подачи 2 запросов к БД.
Мир сложнее, чем он отображен в социальных сетях
В крупных проектах не предусмотрено большого количества связей. Почему? Я не знаю. Возможно человеческая психика еще не готова к этому, но… В частном разговоре с одним моим бывшим преподавателем из местного университета проскользнула мысль: связи между людьми сложнее, чем это представлено в сетях. Есть учителя и ученики, начальники и подчиненные, офицеры и их солдаты, администраторы сайтов, и пользователи, родители и дети и прочее, прочее, прочее.
Заметили? Часто роли людей во взаимосвязи не равнозначны, это вам не просто — друзья. Все немного сложнее.
Так же, как правило у социальной связи есть контекст (жизнь, работа, армия, школа) — т.е. место, где эта связь была установлена.
Что мне не нравится в социальных сетях сейчас? Так это то, что добавляя в друзья случайных знакомых или людей, которых ты знаешь только в лицо, а потом удаляя их из своего «послужного списка» во время ревизии (
Это я веду к тому, что было бы здорово, если бы в соц. сетях были предусмотрены разные варианты — знакомый, спортивный тренер, моя бабушка, одноклассник из школы,
Уделив 45 минут времени Rails 3 я попробовал накидать некий прототип того, что неожиданно взбудоражило мой воспаленный учительский разум.
Модель
Модель (я назову ее Graph) содержит 2 id пользователей (подавшего заявку и получателя заявки), статус заявки, роль отправителя и роль получателя, а так же контекст социальной связи.
rails g model graph context:string sender_id:integer sender_role:string recipient_id:integer recipient_role:string state:string
Что, дает следующую миграцию:
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
Выполним в консоли:
rake db:migrate
Что создаст нам в БД необходимую таблицу с заданными полями.
В самом файле модели Graph я с помощью state machine определил какие состояния может принимать элемент графа, а scope позволит мне дополнить запросы к БД необходимыми условиями.
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
В модель User (она по-любому есть в каждом Rails App) я для начала добавлю метод: graph_to, который вернет мне элемент графа к данному пользователю (если элемент графа существует) или просто создаст новый элемент.
Элемент графа я строю от текущего пользователя до другого пользователя, в некотором контексте, где я являюсь кем-то и получатель так же, является кем-то (согласно предопределенных ролей).
По-умолчанию контекстом является жизнь, а пользователи имеют роли — друг.
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
Для экспериментов потребуется много записей о взаимосвязях пользователей. Поэтому я создал рейк, который из консоли позволяет мне создать несколько десятков пользователей и установить между ними случайные связи.
Поясняю для тех, кто не умеет читать на руби.
- В коде создаются пользователи
- Устанавливаются контексты взаимосвязей
- Для каждого контекста устанавливаются роли пользователей
- Случайным образом подаются заявки и, так же, случайно заявки получают состояние
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
Инвертированные элементы графа
Поскольку я ранее говорил, что не захотел в этот раз создавать каждой социальной связи запись-близнеца, то мне придется воспринимать каждую связь как в прямом, так и в обратном направлении.
Это я сделаю добавив в модель User строки:
has_many :graphs, :foreign_key=>:sender_id
has_many :inverted_graphs, :class_name => 'Graph', :foreign_key=>:recipient_id
Каждый пользователь имеет множество прямых связей (где он является инициатором связи), так и обратных, где он является получателем запроса на социальную связь. Эти элементы отличаются только разными внешними ключами.
Что бы выбирать все социальные связи данного пользователя мне придется выбирать все его прямые и обратные связи, а потом объединять массивы записей. Например, для выборки всех добавленных начальников с моей работы надо написать примерно следующее:
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
Оператор | является оператором объединения массивов. По мне, так очень красиво.
Немного мета-программирования и ruby магии
У меня очень много контекстов и ролей пользователей во взаимосвязях. Мне нужно много методов, подобных вышеизложенному методу accepted_chiefs_from_job который выбирает всех моих начальников с работы, которых я согласился добавить. Вы же не думаете писать их в ручную?
Мы используем мета-программирование, что бы руби сам создавал нам нужные методы и делал соответствующие выборки. Поможет в этом волшебный метод method_missing(method_name, *args). Этот метод вызывается когда руби не находит какой-то метод. Вот тут то мы ему и поясним, что нужно делать в случае, когда он встретит попытку выборки данных из графа.
Руби будет сам создавать методы подобные этим:
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
Добавим в модель User следующее:
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
Если method_missing(method_name, *args) не находит какой-то метод, то он попытается его распарсить по регулярке. Если регулярка подходит под название методов нашего графа, то руби сам состовит запрос по полученным из строки данным и вернет результат. Если вызываемый метод не подходит под регулярку, то method_missing(method_name, *args) просто перейдет к своему стандартном у поведению — super, и, вероятно, даст ошибку выполнения кода.
Итоговый код User:
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
Ну вот и всё
Теперь выполняем рейк:
rake db:graphs:create
Запускаем rails консоль
rails c
Пробуем выполнять:
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:
Прикладным программистам уважение и пожелание удачи от школьного учителя!