# coding: utf-8

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

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

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

naive_user_has_many_following

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

Проблема модели данных из Рис. 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.

user_has_many_following

Рисунок 12.7: Модель слежения пользователя за сообщениями через промежуточную модель (Взаимоотношений) Relationship. (полный размер)

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

$ rails generate model Relationship follower_id:integer followed_id:integer

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

Листинг 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_model

Рисунок 12.8: Модель данных Relationship.

Как и с любой новой моделью, прежде чем двигаться дальше, мы должны определить доступные атрибуты модели. В случае с моделью Relationship, followed_id должен быть доступным, поскольку пользователи будут создавать взаимоотношения через веб, но атрибут follower_id должен быть недоступным; в противном случае злоумышленник может вынудить других пользователей следить за его сообщениями. Результат представлен в Листинге 12.2.

Листинг 12.2. Открытие доступа к followed_id атрибуту модели Relationship (но не к follower_id).

app/models/relationship.rb
class Relationship < ActiveRecord::Base
  attr_accessible :followed_id
end
# coding: utf-8