Опубликован: 27.01.2016 | Доступ: свободный | Студентов: 914 / 58 | Длительность: 23:07:00
Лекция 11:

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

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

Поток сообщений

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

Так как нам предстоит тяжелая работа, особенно важно понимать куда мы будем двигаться. Резюме окончательного потока сообщений пользователя, показанный на рис. 11.5, вновь показан на рис. 11.18.

Набросок пользовательской страницы Home с потоком сообщений.

Рис. 11.18. Набросок пользовательской страницы Home с потоком сообщений.

Мотивация и стратегия

Основная идея потока сообщений проста. рис. 11.19 показывает пример таблицы базы данных microposts и результирующий поток сообщений. Цель потока заключается в вытягивании микросообщений чей user id соответствует пользователям, сообщения которых читает текущий пользователь (и id самого текущего пользователя), как указано стрелками на схеме.

Поток сообщений для пользователя (id 1) читающего сообщения пользователей 2, 7, 8 и 10.

Рис. 11.19. Поток сообщений для пользователя (id 1) читающего сообщения пользователей 2, 7, 8 и 10.

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

Micropost.from_users_followed_by(user)

Хотя мы пока не знаем как реализовать его, мы уже можем написать тесты для его функциональности. Ключевым моментом является проверка всех трех требований к потоку: микросообщения читаемых пользователей и самого пользователя должны быт включены в поток, но в него не должны попадать сообщения пользователей, от чтения которых пользователь отписался. Первые два требования уже представлены в наших тестах: Листинг 10.35 проверяет что собственные микросообщения пользователя представлены в потоке, в то время как микросообщений отписанных пользователей быть не должно. Теперь, когда мы знаем как следить за сообщениями пользователей, мы можем добавить третий тип тестов, в этот раз проверяющих что микросообщения читаемых пользователей представлены в потоке, как это показано в Листинге 11.41.

require 'spec_helper'

describe User do
  .
  .
  .
  describe "micropost associations" do
    before { @user.save }
    let!(:older_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.day.ago)
    end
    let!(:newer_micropost) do
      FactoryGirl.create(:micropost, user: @user, created_at: 1.hour.ago)
    end
    .
    .
    .
    describe "status" do
      let(:unfollowed_post) do
        FactoryGirl.create(:micropost, user: FactoryGirl.create(:user))
      end
      let(:followed_user) { FactoryGirl.create(:user) }

      before do
        @user.follow!(followed_user)
        3.times { followed_user.microposts.create!(content: "Lorem ipsum") }
      end

      its(:feed) { should include(newer_micropost) }
      its(:feed) { should include(older_micropost) }
      its(:feed) { should_not include(unfollowed_post) }
      its(:feed) do
        followed_user.microposts.each do |micropost|
          should include(micropost)
        end
      end
    end
  end
  .
  .
  .
end
Листинг 11.41. Финальные тесты для потока сообщений. spec/models/user_spec.rb

Сам поток сообщений просто перекладывает тяжелую работу на Micropost.from_users_followed_by, как это показано в Листинге 11.42.

class User < ActiveRecord::Base
  .
  .
  .
  def feed
    Micropost.from_users_followed_by(self)
  end
  .
  .
  .
end
Листинг 11.42. Добавление завершенного потока сообщений к модели User. app/models/user.rb

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

Пришло время реализовать Micropost.from_users_followed_by, который мы для простоты будем называть "поток". Поскольку конечный результат довольно сложен, мы будем строить итоговую реализацию потока по кусочкам.

В первую очередь нужно подумать о том, какой вид запроса нам нужен. Что мы хотим сделать, это выбрать из таблицы microposts все микросообщения с id соответствующими пользователям, читаемыми данным пользователем (или самому пользователю). Мы можем схематически написать это следующим образом:

SELECT * FROM microposts
WHERE user_id IN (<list of ids>) OR user_id = <user id>

При написании этого кода мы предположили, что SQL поддерживает ключевое слово IN, что позволяет нам протестировать множественное включение. (К счастью это так.)

Вспомним из предварительной реализации потока в Разделе 10.3.3 что Active Record использует where метод для осуществления вида выбора, показанного выше, что иллюстрирует Листинг 10.36. Там наша выборка была очень простой; мы просто взяли все микросообщения с user id соответствующим текущему пользователю:

Micropost.where("user_id = ?", id)

Здесь мы ожидаем, что он будет более сложным, чем то вроде

where("user_id in (?) OR user_id = ?", following_ids, user)

(Здесь мы в состоянии использовать, согласно Rails конвенции, user вместо user.id; Rails автоматически использует id. Мы также опустили впереди идущую часть Micropost. поскольку мы ожидаем что этот метод будет жить в самой модели Micropost.)

Мы видим из этих условий, что нам нужен массив id пользователей, читаемых данным пользователем (или какой-то эквивалент). Один из способов сделать это заключается в использовании Ruby метода map, доступного на любом "перечисляемом" объекте, т.е., любом объекте (таком как Массив или Хэш), который состоит из коллекции элементов.9Основное требование заключается в том, что перечисляемые объекты должны реализовывать each метод для перебора коллекции. Мы видели пример этого метода в Разделе 4.3.2; он работает следующим образом:

$ rails console
>> [1, 2, 3, 4].map { |i| i.to_s }
=> ["1", "2", "3", "4"]

Ситуации, подобные той, что показана выше, где такой же метод (например, to_s) вызывается на каждый элемент, настолько обычная вещь, что есть сокращенная запись, использующая ампресанд & и символ, соответствующий методу:10На самом деле такая нотация на самом деле изначально была расширением которое Rails вносил в ядро ??языка Ruby; она была настолько полезной, что в настоящее время она включена в сам Ruby. Замечательно, правда?

>> [1, 2, 3, 4].map(&:to_s)
=> ["1", "2", "3", "4"]

Используя метод join (Раздел 4.3.1), мы можем создать строку состоящую из id объединив их через запятую-пробел:

>> [1, 2, 3, 4].map(&:to_s).join(', ')
=> "1, 2, 3, 4"

Мы можем использовать вышеприведенный метод для построения необходимого массива id читаемых пользователей вызвав id на каждом элементе в user.followed_users. Например, для первого пользователя в базе данных этот массив выглядит следующим образом:

>> User.first.followed_users.map(&:id)
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

Фактически, так как конструкции такого вида очень полезны, Active Record обеспечивает ее по умолчанию:

>> User.first.followed_user_ids
=> [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51]

Здесь метод followed_user_ids синтезирован библиотекой Active Record на основе ассоциации has_many :followed_users (Листинг 11.10); в результате, для получения id соответствующих коллекции user.followed_users, нам достаточно добавить _ids к названию ассоциации. Строка id читаемых пользователей тогда будет выглядеть следующим образом:

>> User.first.followed_user_ids.join(', ')
=> "4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42,
43, 44, 45, 46, 47, 48, 49, 50, 51"

Однако при вставке строки в SQL, вам нет надобности делать этого; интерполяция ? заботится об этом за вас (и фактически устраняет некоторые несовместимости связанные с базой данных). Это означает что мы можем использовать

user.followed_user_ids

само по себе.

В этой точке вы можете догадаться что код вроде

Micropost.from_users_followed_by(user)

будет включать в себя метод класса в Micropost классе (конструкция кратко упоминавшаяся в Разделе 4.4.1). Предполагаемая реализация с этими строками представлена в Листинге 11.43.

class Micropost < ActiveRecord::Base
  .
  .
  .
  def self.from_users_followed_by(user)
    followed_user_ids = user.followed_user_ids
    where("user_id IN (?) OR user_id = ?", followed_user_ids, user)
  end
end
Листинг 11.43. Первый дубль для from_users_followed_by метода. app/models/micropost.rb

Хотя обсуждение ведущее к Листингу 11.43 было выдержано в гипотетических тонах, он действительно работает! Вы можете проверить это запустив набор тестов, которые должны пройти:

$ bundle exec rspec spec/

В большинстве приложений этой начальной реализации было бы вполне достаточно для большинства практических целей. Но это не финальная реализация; посмотрим, сможете ли вы догадаться почему, прежде чем перейти к следующему разделу. (Намек: А что если пользователь следит за сообщениями 5000 других пользователей?)

Подзапросы

Как намекалось в последнем разделе, реализация потока сообщений в Section 11.3.2 не очень хорошо масштабируется при большом количестве микросообщений, что скорее всего произойдет если пользователь начнет читать сообщения, скажем, 5000 других пользователей. В этом разделе мы повторно реализуем ленту сообщений способом, который лучше масштабируется с количеством читаемых пользователей.

Проблема с кодом из Раздела 11.3.2 в том что

followed_user_ids = user.followed_user_ids

вытягивает всех читаемых пользователей в память и создает массив длинной во весь список читаемых пользователей. Поскольку условие в Листинге 11.43 на самом деле лишь проверяет включение во множество, должен быть более эффективный способ для этого, да и SQL оптимизирован именно для таких множественных операций. Решение заключается в отправке поиска id читаемых пользователей в базу данных с помощью подзапроса.

Мы начнем с рефакторинга потока немного модифицированным кодом в Листинге 11.44.

class Micropost < ActiveRecord::Base
  .
  .
  .
  # Returns microposts from the users being followed by the given user.
  def self.from_users_followed_by(user)
    followed_user_ids = user.followed_user_ids
    where("user_id IN (:followed_user_ids) OR user_id = :user_id",
          followed_user_ids: followed_user_ids, user_id: user)
  end
end
Листинг 11.44. Улучшение from_users_followed_by. app/models/micropost.rb

В качестве подготовки к следующему шагу мы заменили

where("user_id IN (?) OR user_id = ?", followed_user_ids, user)

на эквивалентное

where("user_id IN (:followed_user_ids) OR user_id = :user_id",
      followed_user_ids: followed_user_ids, user_id: user)

Синтаксис со знаком вопроса хорош, но когда мы хотим ту же переменную вставить более чем в одном месте, второй вариант синтаксиса является более удобным.

Обсуждение выше привело нас к тому что мы добавим второе вхождение user_id в SQL запросе. В частности, мы можем заменить Ruby код

followed_user_ids = user.followed_user_ids

на фрагмент SQL

followed_user_ids = "SELECT followed_id FROM relationships
                     WHERE follower_id = :user_id"

Этот код содержит подзапрос SQL и внутренне вся выборка для пользователя 1 будет выглядеть примерно так:

SELECT * FROM microposts
WHERE user_id IN (SELECT followed_id FROM relationships
                  WHERE follower_id = 1)
      OR user_id = 1

Этот подзапрос организует всю логику для отправки в базу данных, что является более эффективным.11Для более продвинутых способов создания необходимых подзапросов, см. сообщение в блоге "Hacking a subselect in ActiveRecord".

С этим фундаментом мы готовы к эффективной релизации потока сообщений, как видно в Листинге 11.45. Обратите внимание, что, так как теперь это чистый SQL, followed_user_ids является интерполированным, а не маскированным. (На самом деле рабочими являются оба варианта, но мне кажется более логичным интерполировать в данном контексте.)

class Micropost < ActiveRecord::Base
  belongs_to :user
  default_scope -> { order('created_at DESC') }
  validates :content, presence: true, length: { maximum: 140 }
  validates :user_id, presence: true

  # Returns microposts from the users being followed by the given user.
  def self.from_users_followed_by(user)
    followed_user_ids = "SELECT followed_id FROM relationships
                         WHERE follower_id = :user_id"
    where("user_id IN (#{followed_user_ids}) OR user_id = :user_id",
          user_id: user.id)
  end
end
Листинг 11.45. Финальная реализация from_users_followed_by. app/models/micropost.rb

Этот код представляет собой внушительную комбинацию Rails, Ruby, и SQL, но он делает свою работу и делает ее хорошо. (Конечно же, даже подзапрос не является универсальным решением для масштабирования. Для бОльших сайтов, вам, вероятно, потребуется генерировать поток асинхронно с помощью фонового процесса. Такие тонкости масштабирования выходят за рамки данного руководства.)

Новый поток сообщений

С кодом в Листинге 11.45, наш поток сообщений завершен. Напомним, что код для Home страницы, представлен в Листинге 11.46; этот код создает пагинированный поток соответствущих микросообщений для использования в представлении, как видно в рис. 11.20.12Для того чтобы сделать поток сообщений на рис. 11.20 более привлекательным, я добавил несколько дополнительных микросообщений вручную используя Rails консоль. Отметим, что paginate метод фактически достигает цели в методе модели Micropost в Листинге 11.45, организуя вытягивание только 30 микросообщений за раз из базы данных. (Вы можете проверить это изучив SQL выражения в логах сервера разработки.)

class StaticPagesController < ApplicationController

  def home
    if signed_in?
      @micropost  = current_user.microposts.build
      @feed_items = current_user.feed.paginate(page: params[:page])
    end
  end
  .
  .
  .
end
Листинг 11.46. Действие home с пагинированным потоком. app/controllers/static_pages_controller.rb
Home страница с работающим потоком сообщений.

Рис. 11.20. Home страница с работающим потоком сообщений.
< Лекция 10 || Лекция 11: 12345678
Вадим Обозин
Вадим Обозин

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