Здравствуйте!
Мы все привыкли пользоваться социальными сетями. Одной из их основ является установление социально значимых связей между пользователями. Как правило, эти связи — дружба или поклонники (последователи).
Не знаю что на меня нашло, но вернувшись из школы (я работаю учителем) решил попробовать создать на любимых рельсах что-то, что нам мой взгляд могло бы помочь мне реализовать функционал социального графа на школьном сайте. И двумя типами связей я решил не ограничиваться.
Попробуем пофантазировать на тему социального графа и написать немного 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:
Прикладным программистам уважение и пожелание удачи от школьного учителя!
