12.1.1 Проблема с моделью данных (и ее решение)
В качестве первого шага на пути построения модели данных для слежения за сообщениями пользователей, давайте рассмотрим следующий типичный случай. Возьмем, в качестве примера, пользователя, который следит за сообщениями второго пользователя: мы могли бы сказать, что, например, Кальвин читает сообщения Гоббса, и Гоббс читается Кальвином, таким образом, Кальвин является читателем, а Гоббс является читаемым. При использовании дефолтной Rails’ плюрализации, множество таких читаемых пользователей называлось бы followeds, но это безграмотно и неуклюже; вместо этого мы перепишем умолчание и назовем их читаемые, и user.following
будет содержать массив пользователей, за сообщениями которых следит текущий пользователь. Аналогично, множество пользователей, читающих данного пользователя это читатели пользователя, и user.followers
будет массивом таких пользователей.
Это предполагает моделирование читаемых пользователей как на Рис. 12.6, с following
таблицей и has_many
ассоциацией. Поскольку user.following
должно быть массивом пользователей, каждая строка таблицы following
должна быть пользователем, идентифицируемым с помощью followed_id
, совместно с follower_id
для установления ассоциации.2 Кроме того, так как каждая строка является пользователем, мы должны были бы включить другие атрибуты пользователя, включая имя, пароль и т.д.

Проблема модели данных из Рис. 12.6 в том, что она ужасно избыточна: каждая строка содержит не только id каждого читаемого пользователя, но и всю остальную информацию, уже содержащуюся в таблице users
. Еще хуже то, что для моделирования читателей пользователя нам потребуется отдельная followers
таблица. Наконец, эта модель данных кошмарно неудобна в эксплуатации, так как каждый раз, при изменении пользователем (скажем) своего имени, нам пришлось бы обновлять запись пользователя не только в users
таблице, но также каждую строку, содержащую этого пользователя
в обоих following
и followers
таблицах.
Проблема здесь в том, что нам не хватает лежащей в основе абстракции (an underlying abstraction (??)). Один из способов найти правильную абстракцию, это рассмотреть, как мы могли бы реализовать following в веб-приложении. Вспомним из Раздела 6.3.3, что REST архитектура включает в себя ресурсы которые создаются и уничтожаются. Это приводит нас к двум вопросам: Что создается, когда пользователь начинает читать сообщения другого пользователя? Что уничтожается, когда пользователь прекращает следить за сообщениями другого пользователя?
Поразмыслив, мы видим, что в этих случаях приложение должно создать либо разрушить взаимоотношение (или связь3) между двумя пользователями. Затем пользователь has_many :relationships
(имеет_много :взаимоотношений), и имеет много following
(или followers
) через эти взаимоотношения. Действительно, Рис. 12.6 уже содержит бОльшую часть реализации: поскольку каждый читаемый пользователь уникально идентифицирован посредством followed_id
, мы можем преобразовать following
в таблицу relationships
, опустив информацию о пользователе, и использовать followed_id
для получения читаемых пользователей из users
таблицы. Кроме того, приняв во внимание обратные взаимоотношения, мы могли бы использовать follower_id
столбец для извлечения массива читателей пользователя.
Чтобы создать following
массив пользователей, мы могли бы вытянуть массив атрибутов followed_id
а затем найти пользователя для каждого из них. Однако, как и следовало ожидать, в Rails есть более удобный способ для этой процедуры; соответствующая техника известна как has_many :through
(имеет_много :через).4 Как мы увидим в Разделе 12.1.4, Rails позволяет нам сказать, что пользователь следит за сообщениями многих пользователей через взаимоотношения, используя краткий код
has_many :following, :through => :relationships, :source => "followed_id"
Этот код автоматически заполняет user.following
массивом читаемых пользователей. Схема модели данных представлена на Рис. 12.7.

Чтобы начать работу над реализацией, мы сначала генерируем модель Relationship следующим образом:
$ rails generate model Relationship follower_id:integer followed_id:integer
Так как мы будем искать взаимоотношения по follower_id
и по followed_id
, мы должны добавить индекс на каждой колонке для повышения эффективности поиска, как показано в Листинге 12.1.
relationships
таблицы.db/migrate/<timestamp>_create_relationships.rb
class CreateRelationships < ActiveRecord::Migration
def self.up
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
def self.down
drop_table :relationships
end
end
Листинг 12.1 включает также составной индекс, который обеспечивает уникальность пар (follower_id
, followed_id
), так что пользователь не может следить за сообщениями другого пользователя более одного раза:
add_index :relationships, [:follower_id, :followed_id], :unique => true
(Сравните с email индексом уникальности из Листинга 6.22.) Как мы увидим в Разделе 12.1.4, наш пользовательский интерфейс не позволит этому случиться, но добавление индекса уникальности позволит избежать ошибки в случае, если пользователь попытается дублировать взаимотношения любым другим способом (используя, например, инструмент командной строки, такой как cURL). Мы могли бы также добавить валидацию уникальности к модели Relationship, но так как дублирование взаимоотношений является ошибкой всегда, для наших целей вполене достаточно индекса уникальности.
Для создания таблицы relationships
, мы мигрируем базу данных и подготавливаем тестовую бд, как обычно:
$ rake db:migrate
$ rake db:test:prepare
Результирующая модель данных Relationship показана на Рис. 12.8.

Как и с любой новой моделью, прежде чем двигаться дальше, мы должны определить доступные атрибуты модели. В случае с моделью Relationship, followed_id
должен быть доступным, поскольку пользователи будут создавать взаимоотношения через веб, но атрибут follower_id
должен быть недоступным; в противном случае злоумышленник может вынудить других пользователей следить за его сообщениями. Результат представлен в Листинге 12.2.
followed_id
атрибуту модели Relationship (но не к follower_id
).app/models/relationship.rb
class Relationship < ActiveRecord::Base
attr_accessible :followed_id
end