Опубликован: 27.01.2016 | Уровень: для всех | Доступ: платный
Лекция 11:

Слежение за сообщениями пользователей

< Лекция 10 || Лекция 11: 12345678

В этой главе мы завершим ядро примера приложения, добавив социальный слой, что позволит пользователям читать (и не читать) сообщения других пользователей, в результате чего на главной странице каждого пользователя будет отображаться список микросообщений тех пользователей, сообщения которых он читает. Мы также сделаем представления для отображения читающих и читаемых пользователей. Мы узнаем, как смоделировать взаимоотношения между пользователями в Разделе 11.1, а затем сделаем веб-интерфейс в Разделе 11.2 (включая введение в Ajax). Наконец, мы закончим, разработав полнофункциональный поток сообщений в Разделе 11.3.

Эта последняя глава содержит несколько из наиболее сложных материалов учебника, в том числе, сложные модели данных и несколько Ruby / SQL хитростей для создания потока сообщений. С помощью этих примеров вы увидите как Rails может обрабатывать даже весьма сложные модели данных, что должно вам пригодиться, так как вы двигаетесь к разработке собственных приложений с их собственными требованиями. Чтобы помочь с переходом от учебника к самостоятельной разработке, Раздел 11.4 содержит рекомендуемые расширения к ядру примера приложения, а также ссылки на более продвинутые ресурсы.

Как обычно, Git пользователи должны создать новую тему ветки:

$ git checkout -b following-users

Так как материал этой главы особенно сложен, прежде чем писать код, мы улучим момент и сделаем небольшой обзор интерфейса. Как и в предыдущих главах, на этом раннем этапе мы будем представлять страницы используя наброски.1Фотографии для набросков взяты с http://www.flickr.com/photos/john_lustig/2518452221/ и http://www.flickr.com/photos/30775272@N05/2884963755/ Полная последовательность страниц работает следующим образом: пользователь, (John Calvin) начинает на странице своего профиля ( рис. 11.1) и переходит на страницу со списком пользователей ( рис. 11.2) для того, чтобы выбрать пользователя, сообщения которого он будет читать. Calvin переходит на страницу профиля выбранного пользователя, Thomas-а Hobbes-а ( рис. 11.3), кликает по кнопке "Follow", чтобы читать сообщения этого пользователя. Это изменяет кнопку "Follow" на "Unfollow", и увеличивает количество "followers" товарища Hobbes-а на единицу ( рис. 11.4). Вернувшись на свою главную страницу, Calvin теперь видит увеличившееся количество "following" и обнаруживает микросообщения Hobbes-а в своем потоке сообщений ( рис. 11.5). Остальная часть этой главы посвящена реализации этой последовательности.

Профиль текущего пользователя.

Рис. 11.1. Профиль текущего пользователя.
Поиск пользователя для чтения его сообщений.

Рис. 11.2. Поиск пользователя для чтения его сообщений.
Профиль другого пользователя с “Follow” кнопкой.

Рис. 11.3. Профиль другого пользователя с “Follow” кнопкой.
Профиля с “Unfollow” кнопкой и увеличившимся количеством читателей.

Рис. 11.4. Профиля с “Unfollow” кнопкой и увеличившимся количеством читателей.
Главная страница с лентой сообщений и увеличившимся количеством читаемых пользователей.

Рис. 11.5. Главная страница с лентой сообщений и увеличившимся количеством читаемых пользователей.

Модель Relationship

Наш первый шаг в реализации слежения за сообщениями пользователей, заключается в построении модели данных, которая не так проста, как кажется. Naively, кажется, что has_many отношение должно сработать: пользователь has_many (имеет_много) читаемых и has_many (имеет_много) читателей. Как мы увидим, в этом подходе есть проблема, и мы узнаем как ее исправить используя has_many through. Вполне вероятно, что многие идеи этого раздела окажутся непонятыми с первого раза, и может потребоваться некоторое время для осознания довольно сложной модели данных. Если вы обнаружите что запутались, попробуйте пройти главу до конца, а затем прочитать этот раздел еще раз, чтобы прояснить для себя некоторые вещи.

Проблема с моделью данных (и ее решение)

В качестве первого шага на пути построения модели данных для слежения за сообщениями пользователей, давайте рассмотрим следующий типичный случай. Возьмем, в качестве примера, пользователя, который следит за сообщениями второго пользователя: мы могли бы сказать, что, например, Кальвин читает сообщения Гоббса, и Гоббс читается Кальвином, таким образом, Кальвин является читателем, а Гоббс является читаемым. При использовании дефолтной Rails’ плюрализации, множество таких читаемых пользователей называлось бы followers, и user.followers был бы массивом таких пользователей. К сожалению, реверс не сработает: по умолчанию, множество всех читаемых пользователей называлось бы followeds, что является безграмотной неуклюжестью. Мы могли бы назвать их following, но это тоже сомнительная идея: в нормальном английском, "following" это множество людей, следящих за вами, т.e., ваши последователи — с точностью до наоборот от предполагаемого значения. Хотя мы будем использовать "following" в качестве метки, как в "50 following, 75 followers", мы будем использовать "followed users" для самих пользователей, с соответствующим массивом user.followed_users.2Первое издание этой книги использовало терминологию user.following, в которой даже я иногда путался. Благодарю читателя Cosmo Lee за то что он убедил меня изменить терминологию и за сформулированные советы о том как сделать это более понятным. (Однако не последовал его совету в точности, так что если вы все еще путаетесь - это не его вина.)

Это предполагает моделирование читаемых пользователей как на рис. 11.6, с followed_users таблицей и has_many ассоциацией. Поскольку user.followed_users должно быть массивом пользователей, каждая строка таблицы followed_users должна быть пользователем, идентифицируемым с помощью followed_id, совместно с follower_id для установления ассоциации.3Для простоты, рис. 11.6 подавляет id столбец таблицы following. Кроме того, так как каждая строка является пользователем, мы должны были бы включить другие атрибуты пользователя, включая имя, пароль и т.д.

Наивная реализация слежения за сообщениями пользователя.

Рис. 11.6. Наивная реализация слежения за сообщениями пользователя.

Проблема модели данных из рис. 11.6 в том, что она ужасно избыточна: каждая строка содержит не только id каждого читаемого пользователя, но и всю остальную информацию, уже содержащуюся в таблице users. Еще хуже то, что для моделирования читателей пользователя нам потребуется отдельная followers таблица. Наконец, эта модель данных кошмарно неудобна в эксплуатации, так как каждый раз при изменении пользователем (скажем) своего имени, нам пришлось бы обновлять запись пользователя не только в users таблице, но также каждую строку, содержащую этого пользователя в обоих followed_users и followers таблицах.

Проблема здесь в том, что нам не хватает лежащей в основе абстракции. Один из способов найти правильную абстракцию, это рассмотреть, как мы могли бы реализовать чтение сообщений пользователя в веб-приложении. Вспомним из Раздела 7.1.2, что REST архитектура включает в себя ресурсы которые создаются и уничтожаются. Это приводит нас к двум вопросам: Что создается, когда пользователь начинает читать сообщения другого пользователя? Что уничтожается, когда пользователь прекращает следить за сообщениями другого пользователя?

Поразмыслив, мы видим, что в этих случаях приложение должно создать либо разрушить взаимоотношение между двумя пользователями. Затем пользователь has_many :relationships (имеет_много :взаимоотношений), и имеет много followed_users (или followers) через эти взаимоотношения. Действительно, рис. 11.6 уже содержит большую часть реализации: поскольку каждый читаемый пользователь уникально идентифицирован посредством followed_id, мы можем преобразовать followed_users в таблицу relationships, опустив информацию о пользователе, и использовав followed_id для получения читаемых пользователей из users таблицы. Кроме того, приняв во внимание обратные взаимоотношения, мы могли бы использовать follower_id столбец для извлечения массива читателей пользователя.

Для того, чтобы создать массив пользователей followed_users, мы могли бы вытянуть массив атрибутов followed_id, а затем найти пользователя для каждого из них. Однако, как и следовало ожидать, в Rails есть более удобный способ для этой процедуры; соответствующая техника известна как has_many through. Как мы увидим в Разделе 11.1.4, Rails позволяет нам сказать, что пользователь следит за сообщениями многих пользователей через таблицу взаимоотношений, используя краткий код

has_many :followed_users, through: :relationships, source: :followed

Этот код автоматически заполняет user.followed_users массивом читаемых пользователей. Схема модели данных представлена на рис. 11.7.

Модель слежения пользователя за сообщениями через взаимоотношения.

Рис. 11.7. Модель слежения пользователя за сообщениями через взаимоотношения.

Чтобы начать работу над реализацией, мы сначала генерируем модель Relationship следующим образом:

$ rails generate model Relationship follower_id:integer followed_id:integer

Возможно при этом была сгенерирована фабрика Relationship которую вам следует удалить:

$ rm -f spec/factories/relationship.rb

Так как мы будем искать взаимоотношения по follower_id и по followed_id, мы должны добавить индекс на каждой колонке для повышения эффективности поиска, как показано в Листинге 11.1.

class CreateRelationships < ActiveRecord::Migration
  def change
    create_table :relationships do |t|
      t.integer :follower_id
      t.integer :followed_id

      t.timestamps
    end
    add_index :relationships, :follower_id
    add_index :relationships, :followed_id
    add_index :relationships, [:follower_id, :followed_id], unique: true
  end
end
Листинг 11.1. Добавление индексов для relationships таблицы. db/migrate/[timestamp]_create_relationships.rb

Листинге 11.1 включает также составной индекс, который обеспечивает уникальность пар (follower_id, followed_id), так что пользователь не может следить за сообщениями другого пользователя более одного раза:

add_index :relationships, [:follower_id, :followed_id], unique: true

(Сравните с индексом уникальности email из Листинга 6.9.) Как мы увидим в Разделе 11.1.4, наш пользовательский интерфейс не позволит этому случиться, но добавление индекса уникальности позволит избежать ошибки в случае, если пользователь попытается дублировать взаимотношения любым другим способом (используя, например, инструмент командной строки, такой как curl). Мы могли бы также добавить валидацию уникальности к модели Relationship, но, так как дублирование взаимоотношений является ошибкой всегда, для наших целей вполне достаточно индекса уникальности.

Для создания таблицы relationships, мы мигрируем базу данных и подготавливаем тестовую бд, как обычно:

$ bundle exec rake db:migrate
$ bundle exec rake test:prepare

Результирующая модель данных Relationship показана на рис. 11.8.


Рис. 11.8.

Ассоциации пользователь/взаимоотношение

Прежде чем приступить к реализации читателей и читаемых, нам вначале необходимо установить ассоциацию между пользователями и взаимоотношениями. Пользователь has_many (имеет_много) взаимоотношений, и, так как взаимоотношения включают двух пользователей — взаимоотношение belongs_to (принадлежит_к) читающим и читаемым пользователям.

Как и с микросообщениями в Разделе 10.1.3, мы будем создавать новые взаимоотношения используя ассоциацию, с помощью такого кода

user.relationships.build(followed_id: ...)

Мы начнем с базовых тестов валидации показанных в Листинге 11.2.

require 'spec_helper'

describe Relationship do

  let(:follower) { FactoryGirl.create(:user) }
  let(:followed) { FactoryGirl.create(:user) }
  let(:relationship) { follower.relationships.build(followed_id: followed.id) }

  subject { relationship }

  it { should be_valid }
end
Листинг 11.2. Тестирование создания Relationship и атрибутов. spec/models/relationship_spec.rb

Обратите внимание, что, в отличие от тестов для моделей User и Micropost, которые использовали @user и @micropost, соответственно, Листинг 11.2 использует let вместо переменных экземпляра. Отличия между ними редко имеют значение,4Более подробно об этом см. when to use let at Stack Overflow. но я считаю let более чистым решением, нежели переменные экземпляра. Ранее мы применяли переменные экземпляра из-за того что нам важно было как можно раньше ввести переменные экземпляра, а также потому что let является немного более продвинутой техникой.

Мы также должны протестировать атрибут relationships модели User как это показано в Листинге 11.3.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:feed) }
  it { should respond_to(:relationships) }
  .
  .
  .
end
Листинг 11.3. Тестирование атрибута user.relationships. spec/models/user_spec.rb

В этой точке вы, возможно, ожидаете код приложения как в Разделе 10.1.3 - и он действительно похож, но есть одно важное отличие: в случае с моделью Micropost мы могли сказать

class Micropost < ActiveRecord::Base
  belongs_to :user
  .
  .
  .
end

и

class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end

поскольку у таблицы microposts есть атрибут user_id для идентификации пользователя (Раздел 10.1.1). Id используемый таким способом для связи двух таблиц базы данных, известен как внешний ключ, и когда внешним ключом для объекта модели User является user_id, Rails может вывести ассоциацию автоматически: по умолчанию, Rails ожидает внешний ключ в форме <class>_id, где <class> является строчной версией имени класса.5 В данном случае, несмотря на то, что мы по прежнему имеем дело с пользователями, они теперь отождествляются с внешним ключом follower_id, поэтому мы должны сообщить об этом Rails, как показано в Листинге 11.4. 5Если вы заметили что followed_id также идентифицирует пользователя, и обеспокоены ассиметричным обращением с читателями и читаемыми, вы готовы к любым неожиданностям. Мы займемся этим вопросом в Разделе 11.1.5.

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
  .
  .
  .
end
Листинг 11.4. Реализация has_many ассоциации пользователь/взаимоотношение. app/models/user.rb

(Поскольку уничтожение пользователя должно также уничтожить его взаимоотношения мы пошли еще дальше и добавили dependent: :destroy к ассоциации; написание теста на это останется в качестве упражнения (Section 11.5).)

Как и у модели Micropost, у Relationship модели есть belongs_to взаимоотношения с пользователями; в данном случае, объект взаимоотношение принадлежит к обоим follower и followed пользователям, что мы и тестируем в Листинге 11.5.

describe Relationship do
  .
  .
  .
  describe "follower methods" do
    it { should respond_to(:follower) }
    it { should respond_to(:followed) }
    its(:follower) { should eq follower }
    its(:followed) { should eq followed }
  end
end
Листинг 11.5. Тестирование belongs_to ассоциации пользователь/взаимоотношения. spec/models/relationship_spec.rb

Чтобы написать код приложения, мы определяем belongs_to взаимоотношения как обычно. Rails выводит названия внешних ключей из соответствующих символов (т.e., follower_id из :follower, и followed_id из :followed), но, так как нет ни Followed ни Follower моделей, мы должны снабдить их именем класса User. Результат показан в Листинге 11.6. Обратите внимание, что, в отличие от дефолтно сгенерированной модели Relationship, в данном случае доступным является только followed_id.

class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end
Листинг 11.6. Добавление belongs_to ассоциаций к модели Relationship. app/models/relationship.rb

Ассоциация followed на самом деле не потребуется до Раздела 11.1.5, но параллельность структуры читатели/читаемые лучше видна при одновременной реализации.

В этой точке тесты из Листинга 11.2 и Листинга 11.3 должны пройти.

$ bundle exec rspec spec/
< Лекция 10 || Лекция 11: 12345678
Вадим Обозин
Вадим Обозин

Здравствуйте, записался на курс. При этом ставил галочку на "обучаться с тьютором". На email пришло письмо, о том, что записался на самостоятельное изучение курса. Как выбрать тьютора?

Акбар Ахвердов
Акбар Ахвердов
Россия, г. Москва
Артём Зайцев
Артём Зайцев
Украина, ДНР