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

Микросообщения пользователей

< Лекция 9 || Лекция 10: 123456 || Лекция 11 >

В Главе 9 были закончены REST действия для ресурса Users, так что пришло время наконец-то добавить второй ресурс: пользовательские микросообщения.1Технически, в Главе 8 мы обращались c сессиями как с ресурсом, но они не сохранялись в базе данных как пользователи и микросообщения. Эти короткие сообщения, связанные с конкретным пользователем, впервые были показаны (в зачаточной форме) в Главе 2. В этой главе мы сделаем полноценную версию наброска из Раздела 2.3, сконструировав модель данных Micropost, связав ее с моделью User при помощи has_many и belongs_to методов, а затем сделав формы и партиалы, необходимые для манипулирования результатами и их отображения. В Главе 11 мы завершим наш крохотный клон Twitter, добавив понятие слежения за пользователями, с тем чтобы получить поток (feed) их микросообщений.

Если вы используете Git для управления версиями, я предлагаю сделать новую тему ветки, как обычно:

$ git checkout -b user-microposts

Модель Micropost

Мы начнем Microposts ресурс с создания модели Micropost, которая фиксирует основные характеристики микросообщений. Что мы сделаем основываясь на работе, проделанной в Разделе 2.3; как и модель из того раздела, наша новая модель Micropost будет включать валидации и ассоциации с моделью User. В отличие от той модели, данная Micropost модель будет полностью протестирована, а также будет иметь дефолтное упорядочивание и автоматическую деструкцию в случае уничтожения родительского пользователя.

Базовая модель

Модели Micropost необходимы лишь два атрибута: content атрибут, содержащий текст микросообщений,2Атрибут content будет string, но, как кратко отмечалось в Разделе 2.1.2, для более длинных текстовых полей вам следует использовать тип данных text и user_id, связывающий микросообщения с конкретным пользователем. Как и в случае с моделью User (Листинг 6.1), мы генерируем ее используя generate model:

$ rails generate model Micropost content:string user_id:integer

Вполне возможно что помимо всего прочего будет сгенерирована фабрика Micropost которую вам следует удалить так как чуть позже мы создадим ее вручную (Раздел 10.1.4):

$ rm -f spec/factories/microposts.rb

Команда генерации создает миграцию для создания таблицы microposts в базе данных (Листинг 10.1); сравните ее с аналогичной миграцией для таблицы users из Листинга 6.2.

class CreateMicroposts < ActiveRecord::Migration
  def change
    create_table :microposts do |t|
      t.string :content
      t.integer :user_id

      t.timestamps
    end
    add_index :microposts, [:user_id, :created_at]
  end
end
Листинг 10.1. Миграция Micropost. (Обратите внимание на индекс на user_id и created_at.) db/migrate/[timestamp]_create_microposts.rb

Обратите внимание на то, что, поскольку мы ожидаем извлечение всех микросообщений, связанных с данным id пользователя в порядке обратном их созданию, Листинг 10.1 добавляет индексы (Блок 6.2) на столбцы user_id и created_at:

add_index :microposts, [:user_id, :created_at]

Включив столбцы user_id и created_at в виде массива, мы тем самым сказали Rails о необходимости создания multiple key index, это означает что Active Record использует оба ключа одновременно. Обратите также внимание на строку t.timestamps, которая (как указано в Разделе 6.1.1) добавляет волшебные столбцы created_at и updated_at. Мы будем работать со столбцом created_at в Разделе 10.1.4 и Разделе 10.2.1.

Мы начнем с минималистичных тестов для модели Micropost, опираясь на аналогичные тесты для модели User (Листинг 6.5). В частности, мы проверим что объект микросообщений отвечает на атрибуты content и user_id, как это показано в Листинге 10.2.

require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before do
    # Этот код идеоматически некорректен.
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
  end

  subject { @micropost }

  it { should respond_to(:content) }
  it { should respond_to(:user_id) }
end
Листинг 10.2. Начальный спек Micropost. spec/models/micropost_spec.rb

Мы можем получить прохождение этих тестов запустив миграции микросообщений и подготовив тестовую базу данных:

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

В результате получилась модель Micropost со структурой, показанной на рис. 10.1.

Модель данных Micropost.

Рис. 10.1. Модель данных Micropost.

Вам следует проверить что тесты проходят:

$ bundle exec rspec spec/models/micropost_spec.rb

Даже несмотря на проходящие тесты, вы могли заметить этот код:

let(:user) { FactoryGirl.create(:user) }
before do
  # Этот код идеоматически некорректен.
  @micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end

Коментарий указывает на то что код в before блоке идеоматически некорректен. Он работает, но это не "Rails way". Посмотрим, сможете ли вы угадать почему. Мы увидим ответ в Разделе 10.1.3.

Первая валидация

Одним из необходимых аспектов модели Micropost является наличие id пользователя для указания на пользователя написавшего микросообщение. Идеоматически корректным способом сделать это является использование ассоциаций Active Record которые мы реализуем в Разделе 10.1.3. Поскольку это подразумевает небольшой рефактринг, в этом разделе мы напишем тест для отлова возможных регрессий.

Как показано в Листинге 10.3, тест валидации для id пользователя просто устанавливает id равным nil. а затем проверяет что результирующий микропост невалиден. (Сравните с тестами модели User из Листинга 6.8.)

require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before do
    # Этот код идеоматически некорректен.
    @micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
  end

  subject { @micropost }

  it { should respond_to(:content) }
  it { should respond_to(:user_id) }

  it { should be_valid }

  describe "when user_id is not present" do
    before { @micropost.user_id = nil }
    it { should_not be_valid }
  end
end
Листинг 10.3. Тест валидации нового микросообщения. spec/models/micropost_spec.rb

Этот код требует чтобы микросообщение было валидным и тестирует наличие атрибута user_id. Мы можем получить прохождение этих тестов с помощью простой валидации наличия показанной в Листинге 10.4.

class Micropost < ActiveRecord::Base
  validates :user_id, presence: true
end
Листинг 10.4. Валидация для user_id атрибута микросообщения. app/models/micropost.rb

Ассоциации Пользователь/Микросообщения

При построении модели данных для веб-приложений, важно иметь возможность создавать связи между отдельными моделями. В данном случае, каждое микросообщение связано с одним пользователем, а каждый пользователь связан с (потенциально) множеством микросообщений — взаимоотношение вкратце рассмотренное в Разделе 2.3.3 и схематически показанное на Рис. рис. 10.2 и рис. 10.3. В рамках реализации этих связей мы напишем тест для модели Micropost и добавим пару тестов для модели User.

belongs_to отношение между микросообщением и его пользователем.

Рис. 10.2. belongs_to отношение между микросообщением и его пользователем.
has_many отношение между пользователем и его микросообщениями.

Рис. 10.3. has_many отношение между пользователем и его микросообщениями.

Используя belongs_to/has_many ассоциацию определенную в этом разделе, Rails строит методы показанные в Таблице 10.1.

Таблица 10.1. Резюме методов user/micropost ассоциации.
Метод Назначение
micropost.user Возвращает объект User связанный с данным микросообщением.
user.microposts Возвращает массив микросообщений пользователя.
user.microposts.create(arg) Создает микросообщение (user_id = user.id).
user.microposts.create!(arg) Создает микросообщение (бросает исключение в случае неудачи).
user.microposts.build(arg) Возвращает новый объект Micropost (user_id = user.id).

Обратите внимание в Таблице 10.1, что вместо

Micropost.create
Micropost.create!
Micropost.new

мы имеем

user.microposts.create
user.microposts.create!
user.microposts.build

Этот паттерн является каноническим способом создания микросообщений: через их ассоциацию с пользователем. При создании микросообщения таким способом, его user_id автоматически устанавливается правильное значение. В частности, мы можем заменить код

let(:user) { FactoryGirl.create(:user) }
before do
  # Этот код идеоматически некорректен.
  @micropost = Micropost.new(content: "Lorem ipsum", user_id: user.id)
end

из Листинга 10.3 на

let(:user) { FactoryGirl.create(:user) }
before { @micropost = user.microposts.build(content: "Lorem ipsum") }

После того как мы определили правильные ассоциации, получающаяся в результате переменная @micropost будет автоматически иметь user_id эквивалентный связанному с ним пользователю.

Как видно в Таблице 10.1, другим результатом определения ассоциации user/micropost является micropost.user, который просто возвращает пользователя которому принадлежит данное микросообщение. Мы можем протестировать это с помощью методов it и its следующим образом

it { should respond_to(:user) }
its(:user) { should eq user }

Результирующие тесты модели Micropost показаны в Листинге 10.5.

require 'spec_helper'

describe Micropost do

  let(:user) { FactoryGirl.create(:user) }
  before { @micropost = user.microposts.build(content: "Lorem ipsum") }

  subject { @micropost }

  it { should respond_to(:content) }
  it { should respond_to(:user_id) }
  it { should respond_to(:user) }
  its(:user) { should eq user }

  it { should be_valid }

  describe "when user_id is not present" do
   before { @micropost.user_id = nil }
    it { should_not be_valid }
  end
end
Листинг 10.5. Тесты для ассоциации микросообщения/пользователь. spec/models/micropost_spec.rb

На стороне модели User ассоциации, мы отложим более детализированные тесты, до Раздела 10.1.4; пока мы просто протестируем наличие атрибута microposts (Листинг 10.6).

require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com",
                     password: "foobar", password_confirmation: "foobar")
  end

  subject { @user }
  .
  .
  .
  it { should respond_to(:admin) }
  it { should respond_to(:microposts) }
  .
  .
  .
end
Листинг 10.6. Тест на наличие атрибута пользовательских microposts attribute. spec/models/user_spec.rb

После всей этой работы, код для реализации ассоциации до смешного короток: мы можем получить прохождение тестов для Листинга 10.5 и Листинга 10.6 добавив всего две строки: belongs_to :user (Листинг 10.7) и has_many :microposts (Листинг 10.8).

class Micropost < ActiveRecord::Base
  belongs_to :user
  validates :user_id, presence: true
end
Листинг 10.7. Микросообщение пренадлежит пользователю. app/models/micropost.rb
class User < ActiveRecord::Base
  has_many :microposts
  .
  .
  .
end
Листинг 10.8. Пользователь имеет_много микросообщений. app/models/user.rb

В этой точке вам следует сравнить написанное в Таблице 10.1 с кодом в Листинге 10.5 и Листинге 10.6 для того чтобы убедиться что вы понимаете основу природы ассоциаций. Вам также следует проверить что тесты проходят:

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

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

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