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

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

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

Валидации

Прежде чем двигаться дальше, мы добавим пару валидаций модели Relationship для комплектности. Тесты (Листинге 11.7) и код приложения (Листинг 11.8) просты.

describe Relationship do
  .
  .
  .
  describe "when followed id is not present" do
    before { relationship.followed_id = nil }
    it { should_not be_valid }
  end

  describe "when follower id is not present" do
    before { relationship.follower_id = nil }
    it { should_not be_valid }
  end
end
Листинг 11.7. Тестирование валидаций модели Relationship. spec/models/relationship_spec.rb
class Relationship < ActiveRecord::Base
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
  validates :follower_id, presence: true
  validates :followed_id, presence: true
end
Листинг 11.8. Добавление валидаций модели Relationship. app/models/relationship.rb

Читаемые пользователи

Теперь мы переходим к сердцу ассоциаций Relationship: followed_users и followers. Мы начнем с followed_users, как показано в Листинге 11.9.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }
  .
  .
  .
end
Листинг 11.9. Тест для атрибута user.followed_users. spec/models/user_spec.rb

Реализация впервые использует has_many through: пользователь имеет много читаемых (пользователей) через взаимоотношения, как показано на рис. 11.7. По умолчанию, в ассоциации has_many through Rails ищет внешний ключ, соответствующий ассоциации в единственном числе; другими словами, код

has_many :followeds, through: :relationships

будет составлять массив, используя followed_id в таблице relationships. Но, как отмечалось в Разделе 11.1.1, user.followeds это довольно неуклюже; гораздо более естественным будет использование "followed users" в качестве множественного числа для "followed", и написание user.followed_users для массива читаемых пользователей. Естественно, Rails позволяет переопределить умолчание, в данном случае с помощью :source параметра (Листинг 11.10), который явно говорит Rails, что источником массива followed_users является множество followed ids.

class User < ActiveRecord::Base
  has_many :microposts, dependent: :destroy
  has_many :relationships, foreign_key: "follower_id", dependent: :destroy
  has_many :followed_users, through: :relationships, source: :followed
  .
  .
  .
end
Листинг 11.10. Добавление к модели User ассоциации followed_users.app/models/user.rb

Для того, чтобы создавать взаимоотношение с читаемым (пользователем), мы введем служебный метод follow! с тем чтобы мы могли написать user.follow!(other_user). (Этот метод follow!должен работать всегда, так что, как и с create! и с save!, мы обозначаем восклицательным знаком что при неудачном создании будет брошено исключение.) Мы также добавим связанный булев метод following? для того чтобы иметь возможность проверять - читает ли пользователь сообщения других пользователей.6Если у вас есть большой опыт моделирования конкретной предметной области, вы зачастую можете предугадать такие вспомогательные методы, и даже если нет, вы часто обнаруживаете себя за их написанием с целью почистить тесты. Однако в данном случае нормально если вы не угадали их. Разработка програмного обеспечения это обычно итеративный процесс — вы пишете код до тех пор пока он не начинает становиться уродливым, а затем вы рефакторите его — но, для краткости, изложение в учебнике немного сглажено. Тесты в Листинге 11.11 показывают как мы планируем использовать эти методы на практике.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:followed_users) }
  it { should respond_to(:following?) }
  it { should respond_to(:follow!) }
  .
  .
  .
  describe "following" do
    let(:other_user) { FactoryGirl.create(:user) }
    before do
      @user.save
      @user.follow!(other_user)
    end

    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }
  end
end
Листинг 11.11. Тесты для некоторых служебных методов “following”. spec/models/user_spec.rb

В коде приложения, метод following? принимает пользователя, названного other_user и проверяет, существует ли он в базе данных; метод follow! вызывает create! через relationships ассоциацию для создания взаимоотношения с читаемым. Результаты представлены в Листинге 11.12.

class User < ActiveRecord::Base
  .
  .
  .
  def feed
    .
    .
    .
  end

  def following?(other_user)
    relationships.find_by(followed_id: other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end
  .
  .
  .
end
Листинг 11.12. Служебные методы following? и follow!. app/models/user.rb

Отметим, что в Листинге 11.12 мы опустили самого пользователя, написав просто

relationships.create!(...)

вместо эквивалентного кода

self.relationships.create!(...)

Явное включение или невключение self в данном случае дело вкуса.

Конечно, пользователи должны иметь возможность прекратить слежение за сообщениями других пользователей, что приводит нас к немного предсказуемому методу unfollow!, как показано в Листинге 11.13.

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:follow!) }
  it { should respond_to(:unfollow!) }
  .
  .
  .
  describe "following" do
    .
    .
    .
    describe "and unfollowing" do
      before { @user.unfollow!(other_user) }

      it { should_not be_following(other_user) }
      its(:followed_users) { should_not include(other_user) }
    end
  end
end
Листинг 11.13. Тест для прекращения слежения за сообщениями пользователя. spec/models/user_spec.rb

Код для unfollow! прост: нужно просто найти взаимоотношение по followed id и уничтожить его (Листинг 11.14).

class User < ActiveRecord::Base
  .
  .
  .
  def following?(other_user)
    relationships.find_by(followed_id: other_user.id)
  end

  def follow!(other_user)
    relationships.create!(followed_id: other_user.id)
  end

  def unfollow!(other_user)
    relationships.find_by(followed_id: other_user.id).destroy!
  end
  .
  .
  .
end
Листинг 11.14. Прекращение слежения за сообщениями пользователя посредством уничтожения взаимоотношения. app/models/user.rb

Читатели пользователя

Последней частью пазла взаимоотношений является метод user.followers сопутствующий user.followed_users. Вы могли заметить в рис. 11.7 что все сведения, необходимые для извлечения массива читателей уже присутствуют в таблице relationships. Действительно, техника та же, что и для читаемых пользователей, но с реверсированием ролей follower_id и followed_id. Это говорит о том, что, если бы мы смогли как-то организовать таблицу reverse_relationships, поменяв местами эти два столбца ( рис. 11.9), то мы бы с легкостью реализовали user.followers.

Модель данных для читателей пользователя, использующая реверсированную модель Relationship.

Рис. 11.9. Модель данных для читателей пользователя, использующая реверсированную модель Relationship.

Начнем с тестов, веря, что магия Rails выручит нас (когда дело дойдет до реализации) (Листинг 11.15).

require 'spec_helper'

describe User do
  .
  .
  .
  it { should respond_to(:relationships) }
  it { should respond_to(:followed_users) }
  it { should respond_to(:reverse_relationships) }
  it { should respond_to(:followers) }
  .
  .
  .

  describe "following" do
    .
    .
    .
    it { should be_following(other_user) }
    its(:followed_users) { should include(other_user) }

    describe "followed user" do
      subject { other_user }
      its(:followers) { should include(@user) }
    end
    .
    .
    .
  end
end
Листинг 11.15. Тестирование перевернутых взаимоотношений. spec/models/user_spec.rb

Обратите внимание на то, как мы изменили субъект с помощью метода subject, замена @user на other_user, позволяет нам протестировать взаимоотношение с читателями естесственным образом:

subject { other_user }
its(:followers) { should include(@user) }

Как вы наверное подозреваете, мы не будем создавать полную таблицу в базе данных только для того чтобы просто произвести реверс взаимоотношений. Вместо этого мы воспользуемся базовой симметрией между читаемыми и читателями для симуляции таблицы reverse_relationships, передав followed_id в качестве внешнего ключа. Иными словами, там где ассоциация relationships использует внешний ключ follower_id,

has_many :relationships, foreign_key: "follower_id"

ассоциация reverse_relationships использует followed_id:

has_many :reverse_relationships, foreign_key: "followed_id"

Ассоциация followers затем строится через реверсированные взаимоотношения, как показано в Листинге 11.16.

class User < ActiveRecord::Base
  .
  .
  .
  has_many :reverse_relationships, foreign_key: "followed_id",
                                   class_name:  "Relationship",
                                   dependent:   :destroy
  has_many :followers, through: :reverse_relationships, source: :follower
  .
  .
  .
end
Листинг 11.16. Реализация user.followers использующая реверсированные взаимоотношения. app/models/user.rb

(Как и с Листингом 11.4, тест для dependent :destroy остается в качестве упражнения (Раздел 11.5).) Обратите внимание, что мы должны включить имя класса для этой ассоциации, т.e.,

has_many :reverse_relationships, foreign_key: "followed_id",
                                 class_name: "Relationship"

потому что иначе Rails будет искать несуществующий класс ReverseRelationship.

Стоит также отметить, что мы могли бы в этом случае пропустить :source, используя просто

has_many :followers, through: :reverse_relationships

поскольку Rails будет автоматически искать внешний ключ follower_id в данном случае. Я сохранил ключ :source для того чтобы подчеркнуть параллельность со структурой ассоциации has_many :followed_users, но вы можете пропустить его.

С кодом в Листинге 11.16, ассоциации читаемые/читатели завершены, и все тесты должны пройти:

$ bundle exec rspec spec/

Этот раздел предъявил довольно высокие требования к вашим навыкам моделирования данных, и это нормально, если для его усвоения потребуется некоторое время. Фактически, одним из самых лучших способов понять ассоциации является их использование в веб интерфейсе, как мы увидим в следующем разделе.

< Лекция 10 || Лекция 11: 12345678
Вадим Обозин
Вадим Обозин

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

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