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

Моделирование пользователей

Валидация формата

Наши валидации для атрибута name реализуют только минимальные ограничения: любое непустое имя длиной до 51 символов пройдет; но, конечно, атрибут email должен соответствовать более строгим требованиям. До сих пор мы отклоняли только пустой адрес электронной почты; в этом разделе мы потребуем, чтобы адреса электронной почты соответствовали знакомому образцу user@example.com.

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

>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> addresses = %w[user@foo.COM THE_US-ER@foo.bar.org first.last@foo.jp]
=> ["user@foo.COM", "THE_US-ER@foo.bar.org", "first.last@foo.jp"]
>> addresses.each do |address|
?>   puts address
>> end
user@foo.COM
THE_US-ER@foo.bar.org
first.last@foo.jp

Здесь мы выполнили итерации по элементам массива addresses используя each метод (Раздел 4.3.2). Вооружившись этой техникой мы готовы написать несколько базовых тестов для валидации формата электронной почты (Листинге 6.13).

require 'spec_helper'

describe User do

  before do
    @user = User.new(name: "Example User", email: "user@example.com")
  end
  .
  .
  .
  describe "when email format is invalid" do
    it "should be invalid" do
      addresses = %w[user@foo,com user_at_foo.org example.user@foo.
                     foo@bar_baz.com foo@bar+baz.com]
      addresses.each do |invalid_address|
        @user.email = invalid_address
        expect(@user).not_to be_valid
      end
    end
  end

  describe "when email format is valid" do
    it "should be valid" do
      addresses = %w[user@foo.COM A_US-ER@f.b.org frst.lst@foo.jp a+b@baz.cn]
      addresses.each do |valid_address|
        @user.email = valid_address
        expect(@user).to be_valid
      end
    end
  end
end
Листинг 6.13. Тесты для валидации формата адреса электронной почты. spec/models/user_spec.rb

Как было отмечено выше, они не являются исчерпывающими, но мы проверили обычные допустимые формы электронной почты user@foo.COM, THE_US-ER@foo.bar.org (верхний регистр, подчеркивание и соединенные домены) и first.last@foo.jp (стандартное корпоративное имя пользователя first.last, с двухбуквенным доменом верхнего уровня jp (Japan)), наряду с несколькими недопустимыми формами.

Код приложения для валидации формата электронной почты использует регулярное выражение (или regex) для определения формата, наряду с :format аргументом для validates метода (Листинге 6.14).

class User < ActiveRecord::Base
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }
end
Листинг 6.14. Валидация формата адреса электронной почты с регулярным выражением. app/models/user.rb

Здесь регулярное выражение VALID_EMAIL_REGEX это константа, которая обозначается в Ruby именем начинающимся с большой буквы. Код

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }

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

Так, откуда появился образец? Регулярные выражения состоят из краткого (некоторые сказали бы нечитаемого) языка для сравнения текстовых паттернов; изучение построения регулярных выражений это искусство и для начала я разбил VALID_EMAIL_REGEX на небольшие куски (Таблица 6.1).10Обратите внимание, что в Таблица 6.1, "буква" на самом деле означает "строчную букву", но i в конце regex обеспечивает нечувствительность к регистру. Я считаю что замечательный онлайн редактор регулярных выражений Rubular ( рис. 5.4) просто незаменим для изучения регулярных выражений.11Если вы считаете это столь же полезным, как я, призываю вас внести пожертвование чтобы вознаградить разработчика Michael Lovitt за его замечательную работу над Rubular. Cайт Rubular имеет красивый интерактивный интерфейс для создания р егулярных выражений, а также удобную Regex справку. Я призываю вас изучать Таблицу 6.1 с открытым в браузере Rubular-ом. Никакое чтение о регулярных выражениях не может заменить пару часов игры с Rubular. (Примечание: если вы хотите использовать регулярное выражение из Листинга 6.14 в Rubular, вам следует пропустить символы \A и \z.)

Таблица 6.1. Элементы регулярного выражения для email из Листинга 6.14.
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i полное регулярное выражение
/ начало регулярного выражения
\A соответствует началу строки
[\w+\-.]+ по крайней мере один символ слова, плюс, дефис или точка
@ буквально "знак собаки"
[a-z\d\-.]+ по крайней мере одна буква, цифра, дефис или точка
\. буквальная точка
[a-z]+ по крайней мере одна буква
\z соответствует концу строки
/ конец регулярного выражения
i нечувствительность к регистру

Кстати, на самом деле существует полное регулярное выражение для сопоставления адресов электронной почты в соответствии с официальным стандартом, но волноваться не стоит. Экземпляр из Листинга 6.14 тоже хорош, возможно даже лучше чем официальный.12Знаете ли вы, что "Michael Hartl"@example.com, с кавычками и пробелом в середине - является допустимым адресом электронной почты согласно стандарту? Однако выражение приведенное выше имеет один недостаток: оно позволяет невалидные адреса вроде foo@bar..com содержащих последовательно расположенные точки. Исправление этого недочета оставлено в качестве упражнения (Раздел 6.5).

Удивительный редактор регулярных выражений Rubular

Рис. 6.4. Удивительный редактор регулярных выражений Rubular

Теперь тесты должны пройти. (Фактически, тесты для валидных адресов электронной почты должны были проходить все время; так как регулярные выражения, как известно, подвержены ошибкам, действительные испытания электронной почты в основном заключаются в санитарной проверке на VALID_EMAIL_REGEX.) Это означает, что осталось рассмотреть только одно ограничение: обеспечение уникальности адресов электронной почты.

Валидация уникальности

Для обеспечения уникальность адресов электронной почты (так, чтобы мы могли использовать их в качестве имен пользователей) мы будем использовать :unique опцию для validates метода. Но предупреждаю: есть важное предостережение, так что не просто просмотрите раздел, а прочитайте его внимательно.

Мы начнем, как обычно, с наших тестов. В наших предыдущих тестах модели мы, главным образом, использовали User.new, который только создает объект Ruby в памяти, но для тестов уникальности мы фактически должны поместить запись в базу данных.14 (Первый) тест дублирования электронной почты представлен в Листинге 6.15.

require 'spec_helper'

describe User do

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

  subject { @user }
  .
  .
  .
  describe "when email address is already taken" do
    before do
      user_with_same_email = @user.dup
      user_with_same_email.save
    end

    it { should_not be_valid }
  end
end
Листинг 6.15. Тест на отклонение повторяющихся адресов электронной почты. spec/models/user_spec.rb

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

Мы можем получить прохождение теста из Листинга 6.15 с кодом из Листинге 6.16.

class User < ActiveRecord::Base
  .
  .
  .
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: true
end
Листинг 6.16. Валидация уникальности адресов электронной почты. app/models/user.rb

И все же мы не закончили. Адреса электронной почты не чувствительны к регистру — foo@bar.com равен FOO@BAR.COM или FoO@BAr.coM — и наша валидация должна учитывать и этот случай.13Технически, только доменная часть email адресов является нечувствительной к регистру: foo@bar.com на самом деле отличается от Foo@bar.com. На практике, однако, полагаться на этот факт - плохая идея; как было отмечено в about.com, "Поскольку регистрозависимость email адресов может создать много неурядиц, пробем с совместимостью и головных болей, было бы глупым требовать чтобы email адреса набирались исключительно в правильном регистре. Врядли какой-либо из email сервисов или ISP обращает внимание на регистр email адресов, возвращая сообщения в которых email получателя был набран неправильно (в верхнем регистре, например). " Спасибо читателю Riley Moses за указание на этот факт. Мы тестируем на это с помощью кода из Листи нга 6.17.

require 'spec_helper'

describe User do

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

  subject { @user }
  .
  .
  .
  describe "when email address is already taken" do
    before do
      user_with_same_email = @user.dup
      user_with_same_email.email = @user.email.upcase
      user_with_same_email.save
    end

    it { should_not be_valid }
  end
end
Листинг 6.17. Нечувствительный к регистру тест на отклонение дублирующихся адресов электронной почты. spec/models/user_spec.rb

Здесь мы используем upcase метод на строках (описан кратко в Разделе 4.3.2). Этот тест делает то же самое что и первый тест на дублирование адресов электронной почты, но с прописным адресом электронной почты. Если этот тест кажется вам немного абстрактным, запустите консоль:

$ rails console --sandbox
>> user = User.create(name: "Example User", email: "user@example.com")
>> user.email.upcase
=> "USER@EXAMPLE.COM"
>> user_with_same_email = user.dup
>> user_with_same_email.email = user.email.upcase
>> user_with_same_email.valid?
=> true

Конечно, сейчас user_with_same_email.valid? является true, так как это провальный тест, но мы хотим, чтобы оно было false. К счастью, :uniqueness принимает опцию, :case_sensitive, как раз для этой цели (Листинг 6.18).

class User < ActiveRecord::Base
  .
  .
  .
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end
Листинг 6.18. Валидация уникальности адресов электронной почты, игнорирующая регистр. app/models/user.rb

Обратите внимание: мы просто заменили true на case_sensitive: false; Rails в этом случае делает вывод, что :uniqueness должно быть true. В этой точке наше приложение обеспечивает уникальность адресов электронной почты и наш набор тестов должен пройти.

Предостережение уникальности

Есть одна небольшая проблема, предостережение, на которое я ссылался выше:

Использование validates :uniqueness не гарантирует уникальности.

D’oh! Но что может может пойти не так? А вот что:

  1. Алиса регистрируется на сайте, с email адресом alice@wonderland.com.
  2. Алиса случайно кликает, "Submit" дважды, отправляя два запроса в быстрой последовательности.
  3. Затем происходит следующее: первый запрос создает пользователя в памяти, который проходит проверку, второй запрос делает то же самое, первый запрос пользователя сохраняется, второй запрос пользователя сохраняется.
  4. Результат: две пользовательские записи с одинаковыми адресами электронной почты, несмотря на валидацию уникальности.

Если вышеописанная последовательность кажется вам неправдоподобной, поверьте мне, это не так: это происходит на любом Rails сайте со значительным трафиком. К счастью, решение просто в реализации, нам просто необходимо обеспечить уникальность также на уровне базы данных. Наш метод заключается в создании в базе данных индекса столбца электронной почты и последующем требовании уникальности этого индекса.

Индекс адреса электронной почты представляет собой обновление требований к нашей модели данных что (как обсуждалось в Разделе 6.1.1) делается в Rails посредством миграций. Мы видели в Разделе 6.1.1 что генерация модели User автоматически создает новую миграцию (Листинг 6.2); в данном случае мы добавляем структуру к существующей модели, таким образом, мы должны создать миграцию непосредственно, используя migration генератор:

$ rails generate migration add_index_to_users_email

В отличие от миграции для пользователей, миграция уникальности электронной почты не предопределена, таким образом, мы должны заполнить ее содержание кодом из Листинга 6.19.14Конечно, мы могли только отредактировать файл миграции для таблицы users в Листинге 6.2, но для этого потребовалось бы откатить, а затем вновь накатить миграцию базы данных. Rails Way заключается в использовании миграции каждый раз, когда мы обнаруживаем, что нам необходимо изменить модель данных.

class AddIndexToUsersEmail < ActiveRecord::Migration
  def change
    add_index :users, :email, unique: true
  end
end
Листинг 6.19. Миграция для реализации уникальности адреса электронной почты. db/migrate/[timestamp]_add_index_to_users_email.rb

Здесь используется Rails метод add_index для добавления индекса на столбце email таблицы users. Индекс сам по себе не обеспечивает уникальность, но это делает опция unique: true.

Заключительный шаг должен мигрировать базу данных:

$ bundle exec rake db:migrate

(Если это не сработало, попробуйте закрыть все консольные сессии в песочнице, которая может блокировать базу данных, тем самым препятствуя миграции.) Если вам интересно посмотреть на практический результат выполнения этой команды, посмотрите файл db/schema.rb, который теперь должен содержать строку подобную этой:

add_index "users", ["email"], name: "index_users_on_email", unique: true

К сожалению, есть еще одно изменение которое мы должны сделать для того чтобы быть уверенными в уникальности email адресов - все email адреса должны быть в нижнем регистре, прежде чем они будут сохранены в базе данных. Причина заключается в том что не все адаптеры баз данных используют регистрозависимые индексы.15Непосредственные эксперименты со SQLite на моей системе и с PostgreSQL на Heroku показали что этот шаг фактически необходим. Мы можем достигнуть этого с помощью функции обратного вызова, которая является методом, который вызывается в конкретный момент жизни объекта Active Record (см. Rails API). В данном случае мы будем использовать функцию обратного вызова before_save для того чтобы принудить Rails переводить в нижний регистр email атрибут перед сохранением пользователя в базу данных, как это показано в Листинге 6.20.

class User < ActiveRecord::Base
  before_save { self.email = email.downcase }
  .
  .
  .
end
Листинг 6.20. Ensuring email uniqueness by downcasing the email attribute. app/models/user.rb

Код в Листинге 6.20 передает блок в коллбэк before_save и назначает email адрес пользователя равным его текущему значению в нижнем регистре с помощью метода строки downcase. Этот код довольно продвинутый и в этой точке я советую вам просто поверить в то что он работает; если вы все же сомневаетесь, закомментируйте валидацию уникальности из Листинга 6.16 и попробуйте создать пользователей с идентичными email адресами для того чтобы посмотреть на результирующую ошибку. (Мы вновь увидим эту технику в Разделе 8.2.1, где мы будем использовать конвенцию method reference.) Написание теста для кода в Листинге 6.20 остается в качестве упражнения (Раздел 6.5).

Теперь вышеописанный сценарий с Алисой будет хорошо работать: база данных сохранит запись пользователя, основанную на первом запросе, и отвергнет второе сохранение за нарушение уникальности. (Ошибка появится в логе Rails, но в этом нет ничего плохого. Можно даже отловить ActiveRecord::StatementInvalid исключение, но в этом учебном руководстве мы не будем заморачиваться этим шагом.) Добавление этого индекса на атрибут адреса электронной почты преследует вторую цель, кратко рассмотренную в Разделе 6.1.4: он решает проблему эффективности поиска пользователя с помощью find_by (Блок 6.2).

_________________________________________________________

Блок 6.2.Индексы базы данных

При создании столбца в базе данных, важно учитывать, что нам нужно будет найти записи по этому столбцу. Рассмотрим, например, email атрибут созданый миграцией в Листинге 6.2. Когда мы позволим пользователям регистрироваться на сайте, начиная с Главы 7, нам нужно будет иметь возможность находить запись пользователя соответствующую предоставленному адресу электронной почты; в базе данных, основанной, к сожалению, на наивной модели данных, единственный способ найти пользователя по его адресу электронной почты, это просмотреть строку каждого пользователя в базе данных и сравнить его адрес электронной почты с атрибутом предоставленного адреса электронной почты. Это известно в бизнесе баз данных, как full-table scan (полное сканирование таблицы), а для реального сайта с тысячами пользователей это Bad Thing.

Добавление индекса к столбцу электронной почты решает проблему. Чтобы понять индекс базы данных, полезно рассмотреть аналогию с индексом книги. В книге, чтобы найти все вхождения заданной строки, например "foobar", вам придется сканировать каждую страницу в поиске "foobar". С индексом книги, с другой стороны, вы можете просто посмотреть "foobar" в индексе, чтобы увидеть все страницы, содержащие "foobar". Индекс базы данных работает, по сути, аналогичным образом.

_________________________________________________________

Вадим Обозин
Вадим Обозин

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