Ruby on Rails Tutorial
Изучение Веб Разработки на Rails
Michael Hartl
Содержание
- Предисловие к русскому изданию
- Глава 1 От нуля к развертыванию
- Глава 2 demo app
- Глава 3 В основном статические страницы
- Глава 4 Rails-приправленный Ruby
- Глава 5 Заполнение шаблона
- Глава 6 Моделирование пользователей
- Глава 7 Регистрация
- Глава 8 Войти, выйти
- Глава 9 Обновление, демонстрация и удаление пользователей
- Глава 10 Микросообщения пользователей
- Глава 11 Слежение за сообщениями пользователей
Foreword
Моя компания (CD Baby) была одной из первых громко перешедших на Ruby on Rails, а затем еще громче вернувшейся обратно на PHP (Google расскажет вам об этой драме). Эту книгу, написанную Майклом Хартлом так высоко рекомендовали, что я должен был попробовать её, и Ruby on Rails Tutorial это всё, что я использовал, чтобы вернуться к Rails.
Хотя я уже прошел через много книг по Rails, это одна из немногих, что, наконец, зацепила меня. Было много написано книг типа «Путь Rails» — после которых я чувствовал себя неестественно, но после этой книги я наконец почувствовал себя естественно. Это также единственная книга по Rails, которая соблюдает методику «разработка через тестирование» на всем своем протяжении, этот подход строго рекомендуется специалистами, но он никогда не был так чётко продемонстрирован ранее. Наконец, Git, GitHub и Heroku присутствуют в демо-примерах, автор действительно дает вам почувствовать, что он хотел сделать реальный проект. Учебный код примеров не изолирован.
Линейное повествование — отличный формат. Лично я прошел Rails Tutorial в течении трёх долгих дней, делая все примеры и задачи в конце каждой главы. Делайте всё от начала до конца, не прыгая, и вы получите максимальную пользу.
Наслаждайтесь!
Derek Sivers (sivers.org)
Ранее: основатель, CD Baby
В настоящее время: основатель Thoughts Ltd.
Благодарности
Ruby On Rails Учебник во многом обязан моей предыдущей книге по Rails, RailsSpace и, следовательно, моему соавтору Aurelius Prochazka. Я хотел бы поблагодарить Aure как за работу, которую он проделал над прошлой книгой, так и за поддержку этой. Я также хотел бы поблагодарить Debra Williams Cauley, редактора обеих книг RailsSpace и Rails Tutorial; до тех пор, пока она не прекратит брать меня на бейсбол, я буду продолжать писать книги для нее.
Я хотел бы поблагодарить огромное количество Рубистов Rubyists учивших и вдохновлявших меня на протяжении многих лет: David Heinemeier Hansson, Yehuda Katz, Carl Lerche, Jeremy Kemper, Xavier Noria, Ryan Bates, Geoffrey Grosenbach, Peter Cooper, Matt Aimonetti, Gregg Pollack, Wayne E. Seguin, Amy Hoy, Dave Chelimsky, Pat Maddox, Tom Preston-Werner, Chris Wanstrath, Chad Fowler, Josh Susser, Obie Fernandez, Ian McFarland, Steven Bristol, Pratik Naik, Sarah Mei, Sarah Allen, Wolfram Arnold, Alex Chaffee, Giles Bowkett, Evan Dorn, Long Nguyen, James Lindenbaum, Adam Wiggins, Tikhon Bernstam, Ron Evans, Wyatt Greene, Miles Forrest, хороших людей из Pivotal Labs, команду Heroku, thoughtbot ребят, и команду GitHub. Наконец, многих, многих читателей - слишком много чтобы перечислять их здесь - внёсших большое количество предложений по улучшению и сообщивших об ошибках во время написания этой книги, и я с благодарностью признаю их помощь в написании ее настолько хорошей, насколько это было возможно.
Об авторе
Майкл Хартл – автор Ruby on Rails Tutorial, лидирующего введения в веб разработку на Ruby on Rails. Его предыдущий опыт включает в себя написание и разработку RailsSpace - чрезвычайно устаревшего учебника по Rails и разработку Insoshi - некогда популярной, а ныне устаревшей платформы для социальных сетей написанной на Ruby on Rails. В 2011, Майкл получил Ruby Hero Award за его вклад в Ruby сообщество. Он закончил Harvard College, имеет степень Кандидата Физических Наук присвоенную в Caltech и является выпускником предпринимательских курсов Y Combinator.
Копирайт и лицензия
Ruby on Rails Tutorial: Learn Web Development with Rails. Copyright © 2012 by Michael Hartl. Весь исходный код в Ruby on Rails Tutorial доступен под MIT License и Beerware License.
Лицензия MIT
Copyright (c) 2012 Michael Hartl
Данная лицензия разрешает лицам, получившим копию данного программного
обеспечения и сопутствующей документации (в дальнейшем именуемыми
«Программное Обеспечение»), безвозмездно использовать Программное
Обеспечение без ограничений, включая неограниченное право на использование,
копирование, изменение, добавление, публикацию, распространение,
сублицензирование и/или продажу копий Программного Обеспечения, также
как и лицам, которым предоставляется данное Программное Обеспечение,
при соблюдении следующих условий:
Указанное выше уведомление об авторском праве и данные условия должны быть
включены во все копии или значимые части данного Программного Обеспечения.
ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО
ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ
ГАРАНТИЯМИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И
ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ
НЕСУТ ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ ИЛИ ДРУГИХ
ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ,
ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ
ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
/*
* ----------------------------------------------------------------------------
* "ПИВНАЯ ЛИЦЕНЗИЯ" (Ревизия 42):
* Весь код написан Майклом Хартлом. До тех пор пока вы осознаете это,
* вы можете делать с ним все что захотите. Если мы когда нибудь
* встретимся, и если это того стоило, вы можете купить мне
* пиво в ответ.
* ----------------------------------------------------------------------------
*/
Глава 6 Моделирование пользователей
Главу 5 мы закончили созданием страницы-заглушки для регистрации пользователей (Раздел 5.4); в течение следующих четырех глав мы выполним обещание, неявное в этой начинающейся странице регистрации. Первый важный шаг это создание модели данных для пользователей нашего сайта, вместе со способом хранить эти данные. В Главе 7 мы дадим пользователям возможность регистрироваться на нашем сайте и создадим страницу профиля пользователя. Как только пример приложения сможет создавать новых пользователей, мы также позволим им входить и выходить (Глава 8) и в Главе 9 (Раздел 9.2.1) мы узнаем как защитить страницы от несанкцонированного доступа. Взятые вместе, материалы с Главы 6 по Главу 9 разрабатывают полную Rails систему входа и аутентификации. Как вы, возможно, знаете, для Rails существует множество готовых решений для аутентификации; Блок 6.1 поясняет почему разворачивание собственной системы является лучшей идеей.
Это длинная и насыщенная действиями глава и вам она может показаться необычайно сложной, особенно если вы новичок большинстве в моделировании данных. Тем не менее, по ее окончании мы создадим весьма качественную систему для валидации, хранения и извлечения информации о пользователе.
Фактически всем веб-приложениям в настоящее время требуется какая либо система входа и аутентификации. Неудивительно, что у большинства веб-фреймворков есть множество вариантов реализации подобных систем, и Rails не исключение. Примеры систем аутентификации и авторизации включают в себя Clearance, Authlogic, Devise и CanCan (так же как не-Rails-специфичные решения, построенные на основе OpenID или OAuth). Резонный вопрос - почему мы должны изобретать велосипед. Почему бы просто не использовать готовое решение вместо того чтобы прикручивать свое?
С одной стороны, практика показывает, что аутентификация на множестве сайтов требует серьезной кастомизации и модификация стороннего продукта, это, обычно, даже большая работа чем написание собственной системы с нуля. К тому же, готовые решения это “черные ящики”, с весьма загадочными внутренностями; а когда вы пишете свою собственную систему у вас гораздо больше шансов разобраться в ней. Кроме того, последние дополнения к Rails (Раздел 6.3) очень облегчили написание собственной системы аутентификации. Наконец, если вы все же когда-либо решите использовать стороннюю систему, вам будет гораздо проще в ней разобраться если вы прежде имели опыт написания собственной .
Как обычно, если вы пользуетесь Git для контроля версий, сейчас самое время сделать новую ветку для моделирования пользователей:
$ git checkout master
$ git checkout -b modeling-users
(Первая строка здесь только для того чтобы удостовериться, что вы находитесь на master ветке, чтобы тема ветки modeling-users была основана на master ветке. Можно пропустить эту команду, если вы уже находитесь в master ветке.)
6.1 Модель User
Хотя конечная цель следующих трех глав это создание страницы регистрации для нашего сайта (ее набросок показан на Рис. 6.1), в принятии регистрационной информации сейчас хорошего мало, так как нам в настоящий момент попросту негде ее хранить. Таким образом, первый шаг в регистрации пользователей должен создать структуру данных для получения и хранения их информации.
В Rails дефолтную структуру данных для модели данных называют, что достаточно естественно, модель (М. в MVC из Раздела 1.2.6). Дефолтное решение Rails для проблемы персистентности состоит в том, чтобы использовать базу данных для долгосрочного хранения данных и дефолтную библиотеку Active Record для взаимодействия с базой данных.1 Active Record идет с массой методов для создания, хранения и поиска объектов данных, и все они не требуют использования языка структурированных запросов (SQL)2 применяемого реляционными базами данных. Кроме того, у Rails есть функции, называемые миграциями, которые позволяют писать определения данных на чистом Ruby, без необходимости изучать язык определения данных (DDL). Как результат, Rails почти полностью изолирует вас от деталей хранения данных. В этой книге, благодаря использованию SQLite для разработки и PostgreSQL (через Heroku) для развертывания (Раздел 1.4), мы проработали эту тему еще дальше, до точки, где нам едва ли когда-нибудь придется задумываться о том, как Rails хранит данные, даже для рабочих приложений.
6.1.1 Миграции базы данных
Можно вспомнить из Раздела 4.4.5 , что мы уже встречали, в сделанном нами классе User
объекты user с атрибутами name
и email
. Тот класс служил полезным примером, но он испытывал недостаток в критическом свойстве персистентности: когда мы создали объект User в консоли Rails, он исчез, как только мы вышли. Наша цель в этом Разделе состоит в том, чтобы создать модель для пользователей, которые не будут исчезать так легко.
Как и с классом User в Разделе 4.4.5, мы начнем с моделирования пользователя с двумя атрибутами: name
и email
, последний мы будем использовать в качестве уникального имени пользователя.3 (Мы добавим атрибут пароля в Разделе 6.3.) В Листинге 4.9 мы сделали это с помощью Ruby-метода attr_accessor
:
class User
attr_accessor :name, :email
.
.
.
end
Напротив, при использовании Rails, для моделирования пользователей мы не должны идентифицировать атрибуты явно. Как было кратко отмечено выше, для хранения данных Rails по умолчанию использует реляционные базы данных, которые состоят из таблиц составленных из строк, данных, где у каждой строки есть столбцы атрибутов данных. Например, для того, чтобы сохранить пользователей с именами и адресами электронной почты, мы составим таблицу users
со столбцами name
и email
(с каждой строкой, соответствующей одному пользователю). Называя столбцы таким образом, мы позволяем Active Record выводить атрибуты объектов User для нас.
Давайте посмотрим как это работает. (Если это обсуждение становится слишком абстрактным на ваш взгляд, будьте терпеливы; консольные примеры, начинающиеся в Разделе 6.1.3 и скриншоты браузера базы данных на Рис. 6.3 и Рис. 6.6 должны многое прояснить.) Вспомните из Листинга 5.28 что мы создавали контроллер Users (наряду с new
действием) используя команду
$ rails generate controller Users new --no-test-framework
Есть аналогичная команда для создания модели: generate model
. Листинг 6.1 показывает команду для генерации модели User с двумя атрибутами, name
и email
.
$ rails generate model User name:string email:string
invoke active_record
create db/migrate/[timestamp]_create_users.rb
create app/models/user.rb
invoke rspec
create spec/models/user_spec.rb
(Обратите внимание, что, в отличие от множественного соглашения для имен контроллеров, названия моделей - в ед. числе: контроллер Users, но модель User.) Передавая дополнительные параметры name:string
и email:string
, мы говорим Rails о двух желаемых атрибутах, наряду с тем, какого типа эти атрибуты должны быть (в данном случае, string
). Сравните это с включением имен действий в Листинге 3.4 и Листинге 5.28.
Одним из результатов generate
команды в Листинге 6.1 является новый файл, названный migration. Миграции обеспечивают возможность постепенного изменения структуры базы данных, так, чтобы наша модель данных могла адаптироваться к изменяющимся требованиям. В случае модели User, миграция создается автоматически сценарием генерации модели; что создает таблицу users
с двумя столбцами, name
и email
, как это показано в Листинге 6.2. (Мы увидим в Разделе 6.2.5 и еще раз в Разделе 6.3 как создавать миграцию с нуля.)
users
). db/migrate/[timestamp]_create_users.rb
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
Обратите внимание: у названия файла миграции есть префикс в виде временнОй отметки основанной на времени генерации миграции. В первые дни миграций, названия файлов имели префиксы в виде увеличивающихся целых чисел, что приводило к конфликтам в командах разработчиков в случаях когда несколько программистов создавали миграции с совпадающими номерами. Использование временнЫх меток позволило комфортно избегать подобных коллизий.
Сама миграция представляет собой метод change
определяющий изменения которые необходимо внести в базу данных. В случае Листинга 6.2, change
использует Rails метод называемый create_table
для создания таблицы в базе данных для хранения пользователей. Метод create_table
принимает блок (Раздел 4.3.2) с одной переменной блока, в данном случае названной t
(от “table”). Внутри блока метод create_table
использует объект t
для создания name
и email
столбцов в базе данных, оба с типом string
.4 Здесь название таблицы во множественном числе (users
) даже при том, что название модели в ед. числе (User), что отражает лингвистическое соглашение которому следует Rails: модель представляет единственного (отдельного) пользователя, тогда как таблица базы данных состоит из многих пользователей. Заключительная строка в блоке, t.timestamps
, является специальной командой, которая создает два волшебных столбца, называемые created_at
и updated_at
, которые являются временнЫми отметками, которые автоматически записывают, когда данный пользователь создается и обновляется. (Мы увидим конкретные примеры волшебных столбцов в Разделе 6.1.3.) Полная модель данных, представленная этой миграцией, показана на Рис. 6.2.
Мы можем запустить миграцию, известную как “migrating up”, используя rake
команду (Блок 2.1) следующим образом:
$ bundle exec rake db:migrate
(Можно вспомнить, что мы запускали эту команду прежде, в Разделе 2.2.) При первом запуске db:migrate
она создает файл db/development.sqlite3
, который является базой данных SQLite5. Мы можем увидеть структуру базы данных, используя превосходный SQLite Database Browser чтобы открыть файл db/development.sqlite3
(Рис. 6.3); сравните со схемой на Рис. 6.2. Вы могли отметить, что есть один столбец в Рис. 6.3 неучтенный в миграции: столбец id
. Как было вкратце отмечено в Разделе 2.2, этот столбец создается автоматически, и используется Rails в качестве уникального идентификатора каждой строки.
Большинство миграций являются обратимыми, а это означает что мы можем “migrate down” и переделать ее с помощью единственной Rake задачи, называемой db:rollback
:
$ bundle exec rake db:rollback
Под капотом этой Rake задачи происходит выполнение команды drop_table
для удаления таблицы users из базы данных. Причина по которой это работает кроется в том, что метод change
знает что drop_table
это команда обратная create_table
и это означает что способ отката миграции легко определим. В случае необратимых миграций, таких как удаление столбца из базы данных, необходимо определять отдельные up
и down
методы вместо единственного метода change
. Почитайте о миграциях в (rus)Rails Guides дабы составить о них более полное представление.
Если вы откатывали базу данных, migrate up снова перед продолжением:
$ bundle exec rake db:migrate
6.1.2 Файл модели
Мы видели, как генерация модели User в Листинге 6.1 сгенерировала файл миграции (Листинг 6.2) и мы видели на Рис. 6.3 результаты выполнения этой миграции: это обновило файл development.sqlite3
, создав таблицу users
со столбцами id
, name
, email
, created_at
и updated_at
. Листинг 6.1 также создал саму модель; остальная часть этого раздела посвящена ее изучению.
Мы начнем с рассмотрения кода для модели User, которая живет в файле user.rb
в каталоге app/models/
это, мягко выражаясь, очень компактно (Листинг 6.3). (Примечание: строки attr_accessible
не будет если вы используете Rails 3.2.2 или более ранние версии. В этом случае, вам необходимо добавить ее в Разделе 6.1.2.2.)
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
end
Вспомните из Раздела 4.4.2 что синтаксис class User < ActiveRecord::Base
означает что класс User
наследует от ActiveRecord::Base
, таким образом у модели User автоматически есть вся функциональность ActiveRecord::Base
класса. Конечно, знание этого наследования не приносит пользы, если мы не знаем что содержит ActiveRecord::Base
и мы вскоре это исправим. Прежде, чем мы двинемся дальше, тем не менее, необходимо закончить с двумя задачами.
Аннотация модели
Хотя это и не является строго необходимым, вы могли бы счесть удобным аннотировать ваши Rails модели с помощью гема annotate (Листинг 6.4).
annotate
в Gemfile
.
source 'https://rubygems.org'
.
.
.
group :development, :test do
gem 'sqlite3', '1.3.5'
gem 'rspec-rails', '2.11.0'
end
group :development do
gem 'annotate', '2.5.0'
end
group :test do
.
.
.
end
(Мы поместили гем annotate
в блок group :development
(аналогичный group :test
) потому что аннотации не нужны в production приложениях.) Затем мы устанавливаем его с bundle install
:
$ bundle install
Это дает нам команду annotate
, которая просто добавляет комментарии, содержащие модель данных к файлу модели:
$ bundle exec annotate
Annotated (1): User
Результат представлен в Листинге 6.5.
app/models/user.rb
# == Schema Information
#
# Table name: users
#
# id :integer not null, primary key
# name :string(255)
# email :string(255)
# created_at :datetime
# updated_at :datetime
#
class User < ActiveRecord::Base
attr_accessible :name, :email
end
Наличие модели данных, видимой в файлах модели, помогает мне вспомнить, какие атрибуты имеет модель, но будущие листинги кода будут обычно опускать аннотации для краткости. (Обратите внимание - если вы хотите чтобы ваши аннотации были актуальны, вам следует запускать annotate
при каждом изменении модели данных.)
Доступные атрибуты
Давайте еще раз посмотрим на модель User, уделив в этот раз особое внимание строке attr_accessible
(Листинг 6.6). Эта строка говорит Rails какие атрибуты модели доступны, т.e., какие атрибуты могут быть изменены автоматически сторонними пользователями (такими как пользователи, отправляющие запрос через веб-браузер).
name
и email
атрибутам. app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
end
Код в Листинге 6.6 делает совершенно не то что вы подумали. По умолчанию, все атрибуты модели являются доступными. Листинг 6.6 обеспечивает доступность name
и email
атрибутов — и только name
и email
атрибуты являются автоматически доступными для внешних пользователей. Мы увидим почему это является важным в Главе 9: использование attr_accessible
важно для предотвращения уязвимости массового назначения, к сожалению распространенной и часто серьезной дыре в системе безопасности во многих Rails приложениях.
6.1.3 Создание объектов user
Мы проделали хорошую подготовительную работу, теперь пора воспользоваться ее результатами и узнать об Active Record, играя с нашей недавно созданной моделью User. Как и в Главе 4, наш инструмент - консоль Rails. Так как мы (пока) не хотим производить какие либо изменения в нашей базе данных, мы запустим консоль в sandbox (песочнице):
$ rails console --sandbox
Loading development environment in sandbox
Any modifications you make will be rolled back on exit
>>
Как обозначено полезным сообщением “Любые модификации которые вы сделаете откатятся при выходе”, при работе в песочнице, консоль будет “откатывать” (то есть, отменять) любые изменения базы данных, созданные во время сеанса.
В консольной сессии в Разделе 4.4.5 мы создавали нового пользователя с User.new
, к которому мы имели доступ только после подгрузки файла example user из Листинга 4.9. С моделями ситуация иная; как вы можете вспомнить из Раздела 4.4.4, Rails консоль автоматически загружает окружение Rails, которое включает модели. Это означает что мы можем создавать новые объекты user без необходимости подгружать что либо:
>> User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
Мы видим здесь дефолтное представление объекта user, которое выводит на экран те же атрибуты что были показаны в Рис. 6.2 и Листинге 6.5.
Вызванный без параметров, User.new
возвращает объект с nil
атрибутами. В Разделе 4.4.5 мы спроектировали пример класса User таким образом, чтобы он принимал инициализационный хэш для установки атрибутов объекта; такое решение было обусловлено библиотекой Active Record, которая позволяет инициализировать объекты тем же способом:
>> user = User.new(name: "Michael Hartl", email: "[email protected]")
=> #<User id: nil, name: "Michael Hartl", email: "[email protected]",
created_at: nil, updated_at: nil>
Здесь мы видим, что, как и ожидалось, атрибуты имени и адреса электронной почты были установлены.
Если вы следили за development log, вы, возможно, заметили, что новые строки еще не обнаружились. Это связано с тем, что вызов User.new
не касается базы данных; он просто создает новый Ruby объект в памяти. Чтобы сохранить объект user в базе данных, мы вызовем метод save
на переменной user
:
>> user.save
=> true
Метод save
возвращает true
если сохранение успешно выполнилось и false
если сохранение не выполнено. (Сейчас все сохранения должны успешно выполняться; но в Разделе 6.2 мы увидим случаи, когда некоторые из них не сработают.), После сохранения в development log должна появиться строка с командой SQL INSERT INTO "users"
. Из-за множества методов, предоставляемых Active Record, в этой книге нам не потребуется необработанный SQL и я опущу обсуждение команд SQL с этого времени. Но вы можете многому научиться, наблюдая за логами
Вы, возможно, заметили что у нового объекта user были nil
значения для атрибутов id
и волшебных столбцов created_at
и updated_at
. Давайте посмотрим, изменило ли наше save
что-нибудь:
>> user
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
Мы видим что id
было присвоено значение 1
, в то время как волшебным столбцам были присвоены текущие время и дата.6 В настоящий момент, метки "создан" (created) и "обновлен" (updated) идентичны; мы увидим, что они могут отличаться в Разделе 6.1.5.
Как и с классом User в Разделе 4.4.5, экземпляры модели User предоставляют доступ к своим атрибутам, используя точку:7
>> user.name
=> "Michael Hartl"
>> user.email
=> "[email protected]"
>> user.updated_at
=> Tue, 05 Dec 2011 00:57:46 UTC +00:00
Как мы увидим в Главе 7, часто бывает удобно создать и сохранить модель в два приема, как мы это сделали выше, но Active Record также позволяет вам объединить эти действия в один шаг с User.create
:
>> User.create(name: "A Nother", email: "[email protected]")
#<User id: 2, name: "A Nother", email: "[email protected]", created_at:
"2011-12-05 01:05:24", updated_at: "2011-12-05 01:05:24">
>> foo = User.create(name: "Foo", email: "[email protected]")
#<User id: 3, name: "Foo", email: "[email protected]", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">
Обратите внимание: User.create
, вместо того чтобы возвратить true
или false
, возвращает сам объект User который мы можем дополнительно присвоить переменной (такой как foo
во второй команде выше).
Команда, обратная create
это destroy
:
>> foo.destroy
=> #<User id: 3, name: "Foo", email: "[email protected]", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">
Странно, destroy
, как и create
, возвращает рассматриваемый объект, хотя я не могу вспомнить что когда-либо использовал значение, возвращаемое destroy
. Еще более странно то, что destroy
енный объект все еще существует в памяти:
>> foo
=> #<User id: 3, name: "Foo", email: "[email protected]", created_at: "2011-12-05
01:05:42", updated_at: "2011-12-05 01:05:42">
Как мы узнаем, уничтожили ли мы в действительности объект? И как мы можем получить сохраненные и неуничтоженные объекты user из базы данных? Пора узнать, как использовать Active Record, для поиска объектов user.
6.1.4 Поиск объектов user
Active Record предоставляет несколько способов поиска объектов. Давайте используем их, для того, чтобы найти первого пользователя, которого мы создали, и чтобы проверить, что третий пользователь (foo
) был уничтожен. Мы начнем с существующего пользователя:
>> User.find(1)
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
Здесь мы передали id пользователя в User.find
; Active Record вернула пользователя с этим атрибутом id
.
Давайте посмотрим, существует ли пользователь с id
3
в базе данных:
>> User.find(3)
ActiveRecord::RecordNotFound: Couldn't find User with ID=3
Так как мы уничтожили нашего третьего пользователя в Разделе 6.1.3, Active Record не может найти его в базе данных. Вместо этого find
вызывает exception (исключение), которое является способом указать на исключительное событие при выполнении программы, в данном случае, несуществующий Active Record id вызывает исключение ActiveRecord::RecordNotFound
.8
В дополнение к универсальному find
, Active Record также позволяет нам искать пользователей по определенным атрибутами:
>> User.find_by_email("[email protected]")
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
Метод find_by_email
автоматически создается библиотекой Active Record на основе email
атрибута в таблице users
. (Как вы догадываетесь, Active Record также создает метод find_by_name
.) Поскольку мы будем использовать адреса электронной почты в качестве имен пользователей, этот вид find
будет полезен когда мы узнаем, как позволить пользователям регистрироваться на нашем сайте (Глава 7). Если вы беспокоитесь об эффективности find_by_email
при большом количестве пользователей, то вы немного забегаете вперед; мы обсудим эту проблему и ее решение в Разделе 6.2.5.
Мы закончим несколькими более общими способами поиска пользователей. Во-первых, first
:
>> User.first
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
Естественно, first
просто возвращает первого пользователя в базе данных. Есть также all
:
>> User.all
=> [#<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">,
#<User id: 2, name: "A Nother", email: "[email protected]", created_at:
"2011-12-05 01:05:24", updated_at: "2011-12-05 01:05:24">]
Не сюрприз, что all
возвращает массив (Раздел 4.3.1) всех пользователей в базе данных.
6.1.5 Обновление объектов user
После создания объектов мы зачастую хотим их обновить. Есть два основных способа сделать это. Во-первых, мы можем присвоить атрибуты индивидуально, как мы это делали в Разделе 4.4.5:
>> user # Just a reminder about our user's attributes
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-05 00:57:46", updated_at: "2011-12-05 00:57:46">
>> user.email = "[email protected]"
=> "[email protected]"
>> user.save
=> true
Обратите внимание: заключительный шаг необходим, для того чтобы записать изменения в базу данных. Мы можем увидеть, что произойдет без сохранения, используя reload
, которая перезагружает объекты, опираясь на информацию в базе данных:
>> user.email
=> "[email protected]"
>> user.email = "[email protected]"
=> "[email protected]"
>> user.reload.email
=> "[email protected]"
Теперь, когда мы обновили пользователя, волшебные столбцы отличаются, как и обещалось в Разделе 6.1.3:
>> user.created_at
=> "2011-12-05 00:57:46"
>> user.updated_at
=> "2011-12-05 01:37:32"
Второй способ обновить атрибуты заключается в использовании update_attributes
:
>> user.update_attributes(name: "The Dude", email: "[email protected]")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "[email protected]"
Update_attributes
метод принимает хеш атрибутов и в случае успеха выполняет и обновление, и сохранение за один шаг (возвращающая true
чтобы указать что сохранение произошло). Стоит отметить, что, после того как вы определили некоторые атрибуты как доступные, используя attr_accessible
(Раздел 6.1.2.2), только эти атрибуты могут быть включены в вызов update_attributes
. Если вы когда-либо обнаружите, что ваши модели загадочным образом начинают отказываться обновлять определенные столбцы, удостоверьтесь что эти столбцы включаются в вызов attr_accessible
.
6.2 Валидации User
У модели User, которую мы создали в Разделе 6.1 теперь есть рабочие атрибуты name
и email
, но они абсолютно универсальны: любая строка (включая пустую) в настоящий момент допустима. И все же, имена и адреса электронной почты это нечто более определенное. Например, name
не должно быть пробелом, email
должен соответствовать определенному формату, характерному для адресов электронной почты. Кроме того, так как мы будем использовать адреса электронной почты в качестве уникальных имен пользователей при регистрации, мы не должны позволять дублироваться адресам электронной почты в базе данных.
Короче говоря, мы не должны позволить name
и email
быть просто любыми строками; мы должны реализовать определенные ограничения для их значений. Active Record позволяет нам налагать такие ограничения, с помощью validations. В этом разделе мы рассмотрим несколько из наиболее распространенных случаев, применив валидации для наличия, длины, формата и уникальности. В Разделе 6.3.4 мы добавим заключительную общепринятую валидацию, подтверждение. И мы увидим в Разделе 7.3 как валидации дают нам удобные сообщения об ошибках, когда пользователи предоставляют данные, которые нарушают их.
6.2.1 Начальные тесты для пользователей
Как и все прочие фичи этого примера приложения, мы добавим валидации модели User с помощью разработки через тестирование. Поскольку мы не передали флаг
--no-test-framework
при генерации модели User (в отличие, например, от Листинга 5.28), команда в Листинге 6.1 создала начальные спеки для тестирования пользователей, но в данном случае они практически пусты (Листинг 6.7).
spec/models/user_spec.rb
require 'spec_helper'
describe User do
pending "add some examples to (or delete) #{__FILE__}"
end
Здесь просто используется метод pending
для указания на то, что мы должны заполнить спек чем-нибудь полезным. Мы можем увидеть результат его применения запустив спек модели User:
$ bundle exec rspec spec/models/user_spec.rb
*
Finished in 0.01999 seconds
1 example, 0 failures, 1 pending
Pending:
User add some examples to (or delete)
/Users/mhartl/rails_projects/sample_app/spec/models/user_spec.rb
(Not Yet Implemented)
На большинстве систем, ожидающие спеки отображаются желтыми, для указания на то что они находятся как бы между проходящими (зеленый) и провальными (красный) тестами.
Мы последуем совету дефолтного спека и заполним его небольшим количеством RSpec примеров, как это показано в Листинге 6.8.
:name
и :email
атрибутов. spec/models/user_spec.rb
require 'spec_helper'
describe User do
before { @user = User.new(name: "Example User", email: "[email protected]") }
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
end
Блок before
, который мы видели в Листинге 5.27), запускает код внутри блока перед каждым тестом, в данном случае, создавая новую переменную экземпляра @user
с помощью User.new
и валидного инициализационного хэша. Затем
subject { @user }
делает @user
дефолтным cубъектом тестирования, как мы это видели прежде в контексте переменной page
в Разделе 5.3.4.
Два теста в Листинге 6.8 тестируют на наличие name
и email
атрибутов:
it { should respond_to(:name) }
it { should respond_to(:email) }
Эти тесты неявно используют Ruby метод respond_to?
, который принимает символ и возвращает true
в случае если объект отвечает на данный метод или атрибут и возвращает false
в противном случае:
$ rails console --sandbox
>> user = User.new
>> user.respond_to?(:name)
=> true
>> user.respond_to?(:foobar)
=> false
(Вспомните из Раздела 4.2.3 что Ruby использует знак вопроса для обозначения таких true/false булевых методов.) Сами тесты опираются на булевую конвенцию используемую RSpec: код
@user.respond_to?(:name)
может быть протестирован с помощью такого RSpec кода
@user.should respond_to(:name)
Благодаря subject { @user }
, мы можем опустить @user
в тесте, что приведет к
it { should respond_to(:name) }
Такой вид тестов позволяет нам использовать TDD для добавления новых атрибутов и методов к нашей модели User и, в качестве побочного эффекта, мы получаем хорошую спецификацию методов на которые должны отвечать все объекты User
.
В этой точке вам следует проверить что тесты провальны:
$ bundle exec rspec spec/
Несмотря на то что мы создали базу данных для разработки командой rake db:migrate
в Разделе 6.1.1, тесты провальны, поскольку тестовая база данных пока не знает о модели данных (мало того, она пока вообще не существует). Мы можем создать тестовую базу данных с правильной структурой и тем самым получить прохождение тестов с помощью Rake задачи db:test:prepare
:
$ bundle exec rake db:test:prepare
Это просто обеспечивает соответствие между моделью данных базы данных для разработки в db/development.sqlite3
и моделью данных тестовой базы данных в db/test.sqlite3
. Незапуск этой Rake задачи после миграции является частым источником недоразумений. К тому же, иногда тестовая база данных выходит из строя и требуется ее рестартовать. Если ваш набор тестов загадочным образом рухнул, попробуйте запустить rake db:test:prepare
- возможно это решит проблему.
6.2.2 Валидация наличия
Возможно самой элементарной валидацией является валидация наличия, которая просто проверяет что данный атрибут имеется в наличии. Например, в даннам разделе мы убеждаемся что оба name и email поля заполнены прежде чем пользователь будет сохранен в базе данных. В Разделе 7.3.2 мы увидим как распространить это требование на форму регистрации новых пользователей.
Мы начнем с теста на наличие атрибута name
. Хотя первым шагом в TDD является написание провального теста (Раздел 3.2.1), в данном случае мы пока недостаточно знаем о валидации для того чтобы написать годный тест, так что мы вначале напишем валидацию и немного поиграем с ней в консоли, - чтобы побольше узнать о ней. Затем мы закомментируем валидацию, напишем провальный тест и проверим что раскомментирование валидации приводит к прохождению теста. Эта процедура может показаться излишне педантичной для такого простого теста, но я видел много “простых” тестов, которые на самом деле тестировали неправильные вещи; дотошность в TDD это просто единственный способ быть уверенными в том что мы тестируем правильные вещи. (Такая техника закомментирования также полезна при спасении приложения, код которого уже написан, но—quelle horreur! — (# какой ужас - фр.) не имеет тестов.)
Способ валидации наличия атрибута имени заключается в применении метода validates
с аргументом presence: true
, как это показано в Листинге 6.9. Аргумент presence: true
это одноэлементный хэш опций; вспомните из Раздела 4.3.4 что фигурные скобки являются необязательными при передаче хеша в качестве последнего аргумента в методе. (Как отмечено в Разделе 5.1.1, использование хэшэй опций это очень распространенный прием в Rails.)
name
атрибута. app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
validates :name, presence: true
end
Листинг 6.9 возможно выглядит как магия, но validates
это просто метод, как впрочем и attr_accessible
. Эквивалентная Листингу 6.9 формулировка с применением скобок выглядит следующим образом:
class User < ActiveRecord::Base
attr_accessible(:name, :email)
validates(:name, presence: true)
end
Давайте заскочим в консоль чтобы увидеть эффект добавления валидации к нашей модели User:9
$ rails console --sandbox
>> user = User.new(name: "", email: "[email protected]")
>> user.save
=> false
>> user.valid?
=> false
Здесь user.save
возвращает false
, указывая на провальное сохранение. В заключительной команде мы используем valid?
метод, который возвращает false
когда объект приводит к сбою одной или более валидаций, и true
когда все валидации проходят. В данном случае у нас есть только одна валидация, таким образом, мы знаем, какая именно провалилась, но все же не лишним будет в этом убедиться с помощью объекта errors
, генерируемого при отказе:
>> user.errors.full_messages
=> ["Name can't be blank"]
(Сообщение об ошибке - подсказка, говорящая о том что Rails проверяет наличие атрибута, используя blank?
метод, который мы видели в конце Раздела 4.4.3.)
Теперь о провальном тесте. Чтобы гарантировать что наш начальный тест перестанет работать, давайте закомментируем валидацию (Листинг 6.10).
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
# validates :name, presence: true
end
Начальный тест валидации представлен в Листинге 6.11.
name
. spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should be_valid }
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
end
Первый новый тест это просто проверка на то что объект @user
изначально валиден:
it { should be_valid }
Это еще один пример булевой конвенции RSpec которую мы видели ранее в Разделе 6.2.1: в каждом случае, когда объект отвечает на булевый метод foo?
, существует соответствующий тестовый метод с именем be_foo
. В данном случае мы можем протестировать результат вызова
@user.valid?
с помощью
@user.should be_valid
Как и прежде, subject { @user }
позволяет нам опустить @user
, что приводит к
it { should be_valid }
Второй тест вначале назначает пользовательскому имени недопустимое значение, а затем проверяет что получившийся объект @user
невалиден:
describe "when name is not present" do
before { @user.name = " " }
it { should_not be_valid }
end
Здесь используется блок before
для назначения невалидного значения атрибуту name, а затем происходит проверка того что получившийся объект user невалиден.
Теперь необходимо убедиться в том что в данный момент тесты провальны:
$ bundle exec rspec spec/models/user_spec.rb
...F
4 examples, 1 failure
Теперь раскомментируем валидацию (т.е., вернемся от Листинга 6.10 обратно к Листингу 6.9) для того чтобы получить прохождение теста:
$ bundle exec rspec spec/models/user_spec.rb
....
4 examples, 0 failures
Конечно, мы также хотим валидировать наличие адресов электронной почты. Тест (Листинг 6.12) походит на аналогичный тест для атрибута name
.
email
. spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]")
end
.
.
.
describe "when email is not present" do
before { @user.email = " " }
it { should_not be_valid }
end
end
Реализация практически та же, что и в Листинге 6.13.
name
и email
. app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
validates :name, presence: true
validates :email, presence: true
end
Теперь все тесты должны проходить и валидации “наличия” готовы.
6.2.3 Валидация длины
Мы ограничили нашу модель User требованием имени для каждого пользователя, но мы должны пойти еще дальше: имена пользователей будут отображаться на сайте, таким образом, мы должны будем реализовать некоторое ограничение их длины. С работой, проделанной в Разделе 6.2.2, этот шаг легок.
Мы начнем с теста. В выборе максимальной длины нет ничего хитрого; мы просто примем 50
как разумную верхнюю границу, что означает что имена длиной в 51
символ будут слишком длинными (Листинг 6.14).
name
. spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]")
end
.
.
.
describe "when name is too long" do
before { @user.name = "a" * 51 }
it { should_not be_valid }
end
end
Для удобства мы использовали “мультипликацию строки” в Листинге 6.14 для создания строки длиной в 51 символ. Мы можем увидеть как это работает, используя консоль:
>> "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
>> ("a" * 51).length
=> 51
Тест в Листинге 6.14 должен провалиться. Чтобы заставить его пройти, мы должны знать об аргументе валидации, ограничивающим длину, :length
, наряду с :maximum
параметром реализуют верхнюю границу (Listing 6.15).
name
атрибута. app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true
end
С нашим комплектом тестов, вновь проходящим, мы можем идти дальше, к более интересной валидации: валидации формата электронной почты.
6.2.4 Валидация формата
Наши валидации для атрибута name
реализуют только минимальные ограничения: любое непустое имя длиной до 51 символов пройдет; но, конечно, атрибут email
должен соответствовать более строгим требованиям. До сих пор мы отклоняли только пустой адрес электронной почты; в этом разделе мы потребуем, чтобы адреса электронной почты соответствовали знакомому образцу [email protected]
.
Ни тесты, ни валидации не будут исчерпывающими - лишь достаточно хорошими, чтобы принять большую часть допустимых адресов электронной почты и отклонить большинство недопустимых. Мы начнем с пары тестов, включающих наборы допустимых и недопустимых адресов. Чтобы сделать эти наборы, стоит узнать о полезной технике создания массивов строк, как показано в этом консольном сеансе:
>> %w[foo bar baz]
=> ["foo", "bar", "baz"]
>> addresses = %w[[email protected] [email protected] [email protected]]
=> ["[email protected]", "[email protected]", "[email protected]"]
>> addresses.each do |address|
?> puts address
>> end
[email protected]
[email protected]
[email protected]
Здесь мы выполнили итерации по элементам массива addresses
используя each
метод (Раздел 4.3.2). Вооружившись этой техникой мы готовы написать несколько базовых тестов для валидации формата электронной почты (Листинг 6.16).
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]")
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
@user.should_not be_valid
end
end
end
describe "when email format is valid" do
it "should be valid" do
addresses = %w[[email protected] [email protected] [email protected] [email protected]]
addresses.each do |valid_address|
@user.email = valid_address
@user.should be_valid
end
end
end
end
Как было отмечено выше, они не являются исчерпывающими, но мы проверили обычные допустимые формы электронной почты [email protected]
, [email protected]
(верхний регистр, подчеркивание и соединенные домены) и [email protected]
(стандартное корпоративное имя пользователя first.last
, с двухбуквенным доменом верхнего уровня jp
), наряду с несколькими недопустимыми формами.
Код приложения для валидации формата электронной почты использует регулярное выражение (или regex) для определения формата, наряду с :format
аргументом для validates
метода (Листинг 6.17).
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
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
Здесь регулярное выражение 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
является Ruby константой, его значение не может быть изменено.)
Так, откуда появился образец? Регулярные выражения состоят из краткого (некоторые сказали бы нечитаемого) языка для сравнения текстовых шаблонов; изучение построения регулярных выражений это искусство и для начала я разбил VALID_EMAIL_REGEX
на небольшие куски (Таблица 6.1).10 Я считаю что замечательный онлайн редактор регулярных выражений Rubular (Рис. 6.4) просто незаменим для изучения регулярных выражений.11 Cайт Rubular имеет красивый интерактивный интерфейс для создания регулярных выражений, а также удобную Regex справку. Я призываю вас изучать Таблицу 6.1 с открытым в браузере Rubular-ом. Никакое чтение о регулярных выражениях не может заменить пару часов игры с Rubular. (Примечание: если вы хотите использовать регулярное выражение из Листинга 6.17 в Rubular, вам следует пропустить символы \A и \z.)
Выражение | Значение |
---|---|
/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i | полное регулярное выражение |
/ | начало регулярного выражения |
\A | начало соответствия строки |
[\w+\-.]+ | по крайней мере один символ слова, плюс, дефис или точка |
@ | буквально “знак собаки” |
[a-z\d\-.]+ | по крайней мере одна буква, цифра, дефис или точка |
\. | буквальная точка |
[a-z]+ | по крайней мере одна буква |
\z | конец соответствия строки |
/ | конец регулярного выражения |
i | нечувствительность к регистру |
Кстати, на самом деле существует полное регулярное выражение для сопоставления адресов электронной почты в соответствии с официальным стандартом, но волноваться не стоит. Экземпляр из Листинга 6.17 тоже хорош, возможно даже лучше чем официальный.12
Теперь тесты должны пройти. (Фактически, тесты для валидных адресов электронной почты должны были проходить все время; так как регулярные выражения, как известно, подвержены ошибкам, действительные испытания электронной почты в основном заключаются в санитарной проверке на VALID_EMAIL_REGEX
.) Это означает, что осталось только одно ограничение: обеспечение уникальности адресов электронной почты.
6.2.5 Валидация уникальности
Для обеспечения уникальность адресов электронной почты (так, чтобы мы могли использовать их в качестве имен пользователей) мы будем использовать :unique
опцию для validates
метода. Но предупреждаю: есть важное предостережение, так что не просто просмотрите раздел, а прочитайте его внимательно.
Мы начнем, как обычно, с наших тестов. В наших предыдущих тестах модели мы, главным образом, использовали User.new
, который только создает объект Ruby в памяти, но для тестов уникальности мы фактически должны поместить запись в базу данных.13 (Первый) тест дублирования электронной почты представлен в Листинге 6.18.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]")
end
.
.
.
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
Методика заключается в создании пользователя с тем же адресом электронной почты, что и у @user
, чего мы достигаем с помощью @user.dup
, который создает дубликат пользователя с теми же атрибутами. Поскольку мы затем сохраняем этого пользователя, оригинальный @user
будет иметь адрес электронной почты который уже существует в базе данных и, следовательно, он не должен быть валидным.
Мы можем получить прохождение теста из Листинга 6.18 с кодом из Листинга 6.19.
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: true
end
И все же мы не закончили. Адреса электронной почты не чувствительны к регистру — [email protected]
равен [email protected]
или [email protected]
— и наша валидация должна учитывать и этот случай. Мы тестируем на это с помощью кода из Листинга 6.20.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]")
end
.
.
.
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
Здесь мы используем upcase
метод на строках (описан кратко в Разделе 4.3.2). Этот тест делает то же самое что и первый тест на дублирование адресов электронной почты, но с прописным адресом электронной почты. Если этот тест кажется вам немного абстрактным, запустите консоль:
$ rails console --sandbox
>> user = User.create(name: "Example User", email: "[email protected]")
>> user.email.upcase
=> "[email protected]"
>> 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.21).
app/models/user.rb
class User < ActiveRecord::Base
.
.
.
validates :email, presence: true, format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
end
Обратите внимание: мы просто заменили true
на case_sensitive: false
; Rails в этом случае делает вывод, что :uniqueness
должно быть true
. В этой точке наше приложение обеспечивает уникальность адресов электронной почты и наш набор тестов должен пройти.
Предостережение уникальности
Есть одна небольшая проблема, предостережение, на которое я ссылался выше:
Использование validates :uniqueness
не гарантирует уникальности.
D’oh! Но что может может пойти не так? А вот что:
- Алиса регистрируется на сайте, с email адресом [email protected].
- Алиса случайно кликает, “Submit” дважды, отправляя два запроса в быстрой последовательности.
- Затем происходит следующее: первый запрос создает пользователя в памяти, который проходит проверку, второй запрос делает то же самое, первый запрос пользователя сохраняется, второй запрос пользователя сохраняется.
- Результат: две пользовательские записи с одинаковыми адресами электронной почты, несмотря на валидацию уникальности.
Если вышеописанная последовательность кажется вам неправдоподобной, поверьте мне, это не так: это происходит на любом Rails сайте со значительным трафиком. К счастью, решение просто в реализации, нам просто необходимо обеспечить уникальность также на уровне базы данных. Наш метод заключается в создании в базе данных индекса столбца электронной почты и последующем требовании уникальности этого индекса.
Индекс адреса электронной почты представляет собой обновление требований к нашей модели данных что (как обсуждалось в Разделе 6.1.1) делается в Rails посредством миграций. Мы видели в Разделе 6.1.1 что генерация модели User автоматически создает новую миграцию (Листинг 6.2); в данном случае мы добавляем структуру к существующей модели, таким образом, мы должны создать миграцию непосредственно, используя migration
генератор:
$ rails generate migration add_index_to_users_email
В отличие от миграции для пользователей, миграция уникальности электронной почты не предопределена, таким образом, мы должны заполнить ее содержание кодом из Листинга 6.22.14
db/migrate/[timestamp]_add_index_to_users_email.rb
class AddIndexToUsersEmail < ActiveRecord::Migration
def change
add_index :users, :email, unique: true
end
end
Здесь используется 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 Мы можем достигнуть этого с помощью функции обратного вызова, которая является методом, который вызывается
большинстве в конкретный момент жизни объекта Active Record (см. Rails API). В данном случае мы будем использовать функцию обратного вызова before_save
для того чтобы принудить Rails переводить в нижний регистр email атрибут перед сохранением пользователя в базу данных, как это показано в Листинге 6.23.
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email
before_save { |user| user.email = email.downcase }
.
.
.
end
Код в Листинге 6.23 передает блок в коллбэк before_save
и назначает email адрес пользователя равным его текущему значению в нижнем регистре с помощью метода строки downcase
. Этот код довольно продвинутый и в этой точке я советую вам просто поверить в то что он работает; если вы все же сомневаетесь, закомментируйте валидацию уникальности из Листинга 6.19 и попробуйте создать пользователей с идентичными email адресами для того чтобы посмотреть на результирующую ошибку. (Мы вновь увидим эту технику в Разделе 8.2.1.)
Теперь вышеописанный сценарий с Алисой будет хорошо работать: база данных сохранит запись пользователя, основанную на первом запросе, и отвергнет второе сохранение за нарушение уникальности. (Ошибка появится в логе Rails, но в этом нет ничего плохого. Можно даже отловить ActiveRecord::StatementInvalid
исключение — см. Insoshi для примера — но в этом учебном руководстве мы не будем заморачиваться этим шагом.) Добавление этого индекса на атрибут адреса электронной почты преследует вторую цель, кратко рассмотренную в Разделе 6.1.4: он решает проблему эффективности поиска пользователя с помощью find_by_email
(Блок 6.2).
При создании столбца в базе данных, важно учитывать, что нам нужно будет найти записи по этому столбцу. Рассмотрим, например, email
атрибут созданый миграцией в Листинге 6.2. Когда мы позволим пользователям регистрироваться на сайте, начиная с Главы 7, нам нужно будет иметь возможность находить запись пользователя соответствующую предоставленному адресу электронной почты; в базе данных, основанной, к сожалению, на наивной модели данных, единственный способ найти пользователя по его адресу электронной почты, это просмотреть строку каждого пользователя в базе данных и сравнить его адрес электронной почты с атрибутом предоставленного адреса электронной почты. Это известно в бизнесе баз данных, как full-table scan (полное сканирование таблицы), а для реального сайта с тысячами пользователей это Bad Thing.
Добавление индекса к столбцу электронной почты решает проблему. Чтобы понять индекс базы данных, полезно рассмотреть аналогию с индексом книги. В книге, чтобы найти все вхождения заданной строки, например “foobar”, вам придется сканировать каждую страницу в поиске “foobar”. С индексом книги, с другой стороны, вы можете просто посмотреть “foobar” в индексе, чтобы увидеть все страницы, содержащие “foobar”. Индекс базы данных работает, по сути, аналогичным образом.
6.3 Добавление безопасного пароля
В этом разделе мы добавим последний из базовых атрибутов модели User: безопасный пароль используемый для аутентификации пользователей примера приложения. Методика заключается в запросе у каждого пользователя пароля (с подтверждением), а затем сохранении зашифрованной версии пароля в базе данных. Мы также добавим способ аутентификации пользователя опирающийся на данный пароль, метод, который мы будем использовать в Главе 8 для того чтобы дать пользователям возможность входить на сайт.
Метод для аутентификации будет принимать введеный пароль, шифровать его и сравнивать зашифрованное значение с хранящимся в базе данных. Если они совпадают, то введеный пароль корректен и пользователь аутентифицирован. Сравнивая зашифрованные пароли вместо того чтобы сравнивать пароли непосредственно, мы получаем возможность аутентифицировать пользователей не храня в базе данных сами пароли, тем самым избежав серьезной дыры в системе безопасности нашего приложения.
БОльшая часть механики безопасного пароля будет реализовано с помощьо одного Rails-метода называемого has_secure_password
(впервые был представлен в Rails 3.1). Поскольку очень многое в дальнейшем зависит от этого единственного метода, трудно разрабатывать безопасные пароли постепенно. В результате чего, начиная с Раздела 6.3.2 мы напишем большое количество тестов прежде чем какой либо из них пройдет. Если вы начнете вязнуть, я рекомендую набраться терпения и продержаться до конца, в Разделе 6.3.4 нас ждет отличная развязка. (Поскольку скринкасты позволяют демонстрировать более постепенный подход к разработке, заинтересованным читателям следует посмотреть Ruby on Rails Tutorial screencasts для более полного понимания этого материала.)
6.3.1 Зашифрованный пароль
Мы начнем с необходимого изменения модели данных для пользователей, что подразумевает добавление password_digest
столбца в таблицу users
(Рис. 6.5). Название digest пришло из терминологии криптографических хэш функций, а само имя password_digest
необходимо для работы реализации в Разделе 6.3.4. Как следует зашифровав пароль, мы обеспечим невозможность получения доступа к сайту атакером, даже если он умудрится получить копию базы данных.
Мы будем использовать самую новомодную хэш функцию, называемую bcrypt для необратимого шифрования пароля в виде хэша пароля. Для того чтобы использовать bcrypt в примере приложения нам необходимо добавить гем bcrypt-ruby
в наш Gemfile
(Листинг 6.24).
bcrypt-ruby
в Gemfile
.
source 'https://rubygems.org'
gem 'rails', '3.2.13'
gem 'bootstrap-sass', '2.1'
gem 'bcrypt-ruby', '3.0.1'
.
.
.
Затем запускаем bundle install
:
$ bundle install
На некоторых системах вы можете получить предупреждение
make: /usr/bin/gcc-4.2: No such file or directory
Для того чтобы исправить это, переустановите RVM используя флаг clang
:
$ rvm reinstall 1.9.3 --with-gcc=clang
Поскольку мы хотим чтобы пользователи имели столбец password digest, объект user должен отвечать на password_digest
, что приводит нас к тесту показанному в Листинге 6.25.
password_digest
. spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
.
.
.
end
Для того чтобы получить прохождение этого теста мы вначале генерируем соответствующую миграцию для столбца password_digest
:
$ rails generate migration add_password_digest_to_users password_digest:string
Здесь первый аргумент это название миграции и мы также предоставили второй аргумент с названием и типом атрибута который мы хотим создать. (Сравните это с начальной генерацией таблицы users
в Листинге 6.1.) Мы можем выбрать любое название для миграции, но было бы удобно, если бы ее название заканчивалось на _to_users
, поскольку в этом случае Rails автоматически построит миграцию для добавления столбцов к таблице users
. Кроме того, включив второй аргумент, мы дали Rails достаточно информации для построения для нас всей миграции, как это видно в Листинге 6.26.
password_digest
к таблице users
. db/migrate/[ts]_add_password_digest_to_users.rb
class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
Этот код использует метод add_column
для добавления столбца password_digest
к таблице users
.
Мы можем получить прохождение провального теста из Листинга 6.25 запустив миграцию базы данных разработки и подготовив тестовую базу данных:
$ bundle exec rake db:migrate
$ bundle exec rake db:test:prepare
$ bundle exec rspec spec/
6.3.2 Пароль и подтверждение
Как видно на наброске Рис. 6.1, мы ожидаем что пользователи должны будут подтверждать их пароли, что является общепринятой в сети практикой минимизирующей риск опечаток при введении пароля. Мы можем реализовать это на уровне контроллера, но принято делать это в модели и использовать Active Record для наложения этого ограничения. Метод заключается в добавлении password
и password_confirmation
атрибутов к модели User и последующем требовании совпадения этих двух атрибутов перед сохранением записи в базе данных. В отличие от всех остальных атрибутов, что мы видели до этого, атрибуты пароля будут виртуальными — они будут лишь временно существовать в памяти и не будут постоянно храниться в базе данных. Как мы увидим в Разделе 6.3.4, has_secure_password
реализует эти виртуальные атрибуты автоматически .
Мы начнем с respond_to
тестов для пароля и его подтверждения, как это показано в Листинге 6.27.
password
и password_confirmation
. spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
.
.
.
end
обратите внимание - мы добавили :password
и :password_confirmation
в хэш инициализации для User.new
:
before do
@user = User.new(name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar")
end
Мы определенно не хотим чтобы пользователи могли вводить пустые пароли, так что мы добавим еще один тест для валидации наличия пароля:
describe "when password is not present" do
before { @user.password = @user.password_confirmation = " " }
it { should_not be_valid }
end
Поскольку мы будем вскоре тестировать несовпадение пароля, здесь мы будем тестировать существование валидации наличия установив пароль и его подтверждение равными чистой строке. Здесь используется возможность Ruby делать более чем одно назначение в строке. Например, в консоли мы можем назначить a
и b
равными 3
следующим образом:
>> a = b = 3
>> a
=> 3
>> b
=> 3
В данном случае мы использовали этот способ для назначения обоих атрибутов пароля равными " "
:
@user.password = @user.password_confirmation = " "
Мы также хотим убедиться что пароль и его подтверждение совпадают. Случай, когда они совпадают покрыт с помощью it { should be_valid }
, так что нам осталось протестировать только случай несовпадения:
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
В принципе, мы закончили, но есть один случай который мы не обработали. Что если подтверждение пароля пустое? Если оно пустое или состоит из пробелов, но при этом пароль валиден, значит они не совпадают и валидация подтверждения отловит этот случай. Если и пароль и его подтверждение пусты или состоят из пробелов, валидация наличия пароля отловит и этот случай. К сожалению, есть еще один возможный вариант, вариант при котором подтверждение пароля является nil. Это невозможно реализовать через веб-интерфейс, но вполне возможно сделать в консоли:
$ rails console
>> User.create(name: "Michael Hartl", email: "[email protected]",
?> password: "foobar", password_confirmation: nil)
Если подтверждение пароля является nil
, Rails не запускает валидацию подтверждения, что означает, что мы можем создать в консоли пользователя без подтверждения пароля. (Конечно, прямо сейчас мы еще не добавили валидации и вышеприведеный код будет работать в любом случае.) Для того чтобы избежать этого, мы добавим тест который отловит этот случай:
describe "when password confirmation is nil" do
before { @user.password_confirmation = nil }
it { should_not be_valid }
end
(Такое поведение видится мне незначительным багом в Rails и возможно он будет исправлен в будущей версии, в любом случае, добавление валидации не повредит.)
Собрав все вместе, мы получаем (провальные) тесты в Листинге 6.28. Как было отмечено во введении к этому разделу - трудно разрабатывать безопасные пароли постепенно из-за большого количества фич запрятанных в has_secure_password
, так что в этой точке все вновь добавленные тесты являются провальными. Мы получим их прохождение в Разделе 6.3.4.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
it { should respond_to(:name) }
it { should respond_to(:email) }
it { should respond_to(:password_digest) }
it { should respond_to(:password) }
it { should respond_to(:password_confirmation) }
it { should be_valid }
.
.
.
describe "when password is not present" do
before { @user.password = @user.password_confirmation = " " }
it { should_not be_valid }
end
describe "when password doesn't match confirmation" do
before { @user.password_confirmation = "mismatch" }
it { should_not be_valid }
end
describe "when password confirmation is nil" do
before { @user.password_confirmation = nil }
it { should_not be_valid }
end
end
6.3.3 Аутентификация пользователя
Последняя часть механики наших паролей это метод для получения пользователей по их email и паролям. Эта задача естественным образом разбивается на две части: первая из них это поиск пользователя по адресу электронной почты; вторая это аутентификация пользователя с данным паролем.
Первый шаг прост; как мы видели в Разделе 6.1.4, мы можем найти пользователя с данным адресом электронной почты с помощью метода find_by_email
:
user = User.find_by_email(email)
Второй шаг заключается в применении метода authenticate
для проверки того что у пользователя есть данный пароль. В Главе 8, мы будем получать текущего (вошедшего) пользователя используя код вроде этого:
current_user = user.authenticate(password)
Если данный пароль совпадает с паролем пользователя, он должен вернуть пользователя; в противном случае он должен вернуть false
.
Как обычно, мы можем выразить требования для authenticate
используя RSpec. Получившиеся в результате тесты являются немного более продвинутыми чем те что мы видели до этого, так что давайте разобьем их на части; если вы новичок
большинстве в RSpec, вам возможно понадобится прочитать этот раздел несколько раз. Мы начнем с того, что объект User должен отвечать на authenticate
:
it { should respond_to(:authenticate) }
Затем мы покрываем два случая - совпадение и несовпадения пароля:
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by_email(@user.email) }
describe "with valid password" do
it { should == found_user.authenticate(@user.password) }
end
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not == user_for_invalid_password }
specify { user_for_invalid_password.should be_false }
end
end
Блок before
сохраняет пользователя в базе данных, так что он может быть получен с помощью find_by_email
, чего мы достигаем используя let
method:
let(:found_user) { User.find_by_email(@user.email) }
Мы уже использовали let
в нескольких упражнениях, это первый случай когда мы его видим в основном тексте учебника. Блок 6.3 рассказывает о let
более подробно.
Два блока describe
покрывают случаи когда @user
и found_user
должны быть одинаковыми (совпадение пароля) и разными (несовпадение пароля); они используют “двойное равенства” ==
для проверки эквивалентности объектов (Раздел 4.3.1). Обратите внимание что тесты в
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not == user_for_invalid_password }
specify { user_for_invalid_password.should be_false }
end
используют let
второй раз, а также используют метод specify
. Это просто синоним для it
, который может быть использован когда it
звучит ненатурально. В данном случае, хорошо звучит “it [т.е., пользователь] should not equal wrong user”, но “user: user with invalid password should be false” звучало бы странно; “specify: user with invalid password should be false” звучит лучше.
RSpec метод let обеспечивает удобный способ для создания локальных переменных внутри тестов. Синтаксис может выглядеть немного странно, но его эффект аналогичен назначению переменной. Аргументом let является символ и он принимает блок, который возвращает значение назначаемое локальной переменной с именем символа. Другими большинстве словами,
let(:found_user) { User.find_by_email(@user.email) }
создает переменную found_user чье значение эквивалентно результату find_by_email. Затем мы можем использовать эту переменную в любом before или it блоке на протяжении всего оставшегося теста. Одно из достоинств let заключается в том, что он мемоизирует свое значение, это означает что он запоминает значение от одного вызова до другого. (Обратите внимание, что memoize это технический термин; в частности, его не надо путать с “memorize”.) В данном случае, поскольку let мемоизирует переменную found_user, метод find_by_email будет вызван лишь единожды при запуске спеков модели User.
Наконец, в качестве дополнительной меры предосторожности, мы протестируем на наличие валидации длины паролей, установив длину паролей не меньшей чем шесть знаков:
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
Собрав вместе все тесты мы получаем Листинг 6.29.
authenticate
. spec/models/user_spec.rb
require 'spec_helper'
describe User do
before do
@user = User.new(name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar")
end
subject { @user }
.
.
.
it { should respond_to(:authenticate) }
.
.
.
describe "with a password that's too short" do
before { @user.password = @user.password_confirmation = "a" * 5 }
it { should be_invalid }
end
describe "return value of authenticate method" do
before { @user.save }
let(:found_user) { User.find_by_email(@user.email) }
describe "with valid password" do
it { should == found_user.authenticate(@user.password) }
end
describe "with invalid password" do
let(:user_for_invalid_password) { found_user.authenticate("invalid") }
it { should_not == user_for_invalid_password }
specify { user_for_invalid_password.should be_false }
end
end
end
Как было отмечено в Блоке 6.3, let
мемоизирует свое значение, так что первый вложенный describe
блок в Листинге 6.29 вызывает let
для получения пользователя из базы данных с помощью find_by_email
, но второй describe
блок уже не обращается к базе данных.
6.3.4 У пользователя есть безопасный пароль
В предыдущей версии Rails, добавление безопасного пароля было сложным и долгим, как это можно увидеть в Rails 3.0 версии Rails Tutorial16, где описано создание аутентификационной системы с нуля. Но понимание веб-разработчиками того как лучше всего аутентифицировать пользователей созрело настолько, что она (аутентификация) теперь поставляется в комплекте с последней версией Rails. В результате чего мы закончим реализацию безопасных паролей (и получим зеленый набор тестов) используя лишь несколько строк кода.
Во-первых, нам необходимо сделать столбцы password
и password_confirmation
доступными (Раздел 6.1.2.2) чтобы мы могли создать экземпляр нового пользователя с помощью инициализационного хэша:
@user = User.new(name: "Example User", email: "[email protected]",
password: "foobar", password_confirmation: "foobar")
Следуя модели в Листинге 6.6, мы сделаем это добавив соответствующие символы к списку доступных атрибутов:
attr_accessible :name, :email, :password, :password_confirmation
Во-вторых, нам необходима валидация длины для пароля, которая использует ключ :minimum
по аналогии с ключом :maximum
из Листинга 6.15:
validates :password, presence: true, length: { minimum: 6 }
Затем нам нужно добавить к атрибутам password
и password_confirmation
требование наличия пароля, требование их совпадения и добавить authenticate
метод для сравнения зашифрованного пароля с password_digest
для аутентификации пользователей. Это единственный непростой шаг и в последней версии Rails все эти фичи бесплатно поставляются в одном методе - has_secure_password
:
has_secure_password
Пока столбец password_digest
присутствует в базе данных, добавление одного лишь этого метода к нашей модели дает нам безопасный способ для создания и аутентификации новых пользователей.
(Если вы хотите увидеть как реализован has_secure_password
, я советую взглянуть на хорошо документированый и вполне читабельный исходный код secure_password.rb. Этот код включает строку
validates_confirmation_of :password
которая (как описано в Rails API) автомагически создает атрибут password_confirmation
. Он также включает валидацию для password_digest
атрибута. В Главе 7, мы увидим что это довольно неоднозначное решение.)
Наконец, нам необходима валидация наличия для подтверждения пароля:
validates :password_confirmation, presence: true
Собрав эти три элемента вместе мы приходим к модели User показанной в Листинге 6.30, которая завершает реализацию безопасных паролей.
app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation
has_secure_password
before_save { |user| user.email = email.downcase }
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 },
uniqueness: { case_sensitive: false }
validates :password, presence: true, length: { minimum: 6 }
validates :password_confirmation, presence: true
end
Теперь необходимо убедиться что набор тестов проходит:
$ bundle exec rspec spec/
6.3.5 Создание пользователя
Теперь, когда базовая модель User завершена, мы создадим пользователя в базе данных в качестве подготовки к созданию страницы показывающей информацию о пользователе в Разделе 7.1. Это также даст нам шанс сделать работу, проделанную в предыдущих разделах, более ощутимой; одни лишь проходяшие тесты могут показаться разочаровывающим результатом - гораздо приятнее увидеть настоящую запись пользователя в базе данных.
Поскольку мы пока не можем зарегистрироваться через веб-интерфейс — это является целью Главы 7 — мы будем использовать Rails консоль для создания нового пользователя вручную. В отличие от Раздела 6.1.3, в этом разделе мы не должны работать в песочнице, поскольку в этот раз нашей целью является сохранение записи в базе данных:
$ rails console
>> User.create(name: "Michael Hartl", email: "[email protected]",
?> password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-07 03:38:14", updated_at: "2011-12-07 03:38:14",
password_digest: "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJgjlw0G4f21y...">
Для того чтобы проверить что это работает, давайте взглянем на строку в девелопмент базде данных (db/development.sqlite3
) с помощью SQLite Database Browser (Рис. 6.6). Обратите внимание, что столбцы соответствуют атрибутам модели данных, определенной на Рис. 6.5.
Вернувшись в консоль, мы пожем увидеть эффект has_secure_password
из Листинга 6.30 посмотрев на атрибут password_digest
:
>> user = User.find_by_email("[email protected]")
>> user.password_digest
=> "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJgjlw0G4f21yZIMSH/xoy"
Это зашифрованная версия пароля ("foobar"
) которая была использована для инициализации объекта user. Мы также можем проверить что команда authenticate
работает, использовав вначале невалидный, а затем валидный пароли:
>> user.authenticate("invalid")
=> false
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "[email protected]",
created_at: "2011-12-07 03:38:14", updated_at: "2011-12-07 03:38:14",
password_digest: "$2a$10$P9OnzpdCON80yuMVk3jGr.LMA16VwOExJgjlw0G4f21y...">
Как и требовалось, authenticate
возвращает false
если пароль невалиден и самого пользователя в противном случае.
6.4 Заключение
Начав с нуля, в этой главе мы создали рабочую модель User с name
, email
и различными "парольными" атрибутами, вместе с валидациями обеспечивающими несколько важных ограничений на их значения. Кроме того, мы можем безопасно аутентифицировать пользователей с помощью пароля. В предыдущих версиях Rails такой подвиг потребовал бы в два раза большего количества кода, но благодаря компактному методу validates
и has_secure_password
, мы смогли построить рабочую модель User всего лишь в десять строк исходного кода.
В следующей (седьмой) главе мы сделаем рабочую форму регистрации для создания новых пользователей, вместе со страницей для отображения информации о каждом пользователе. В Главе 8 мы будем использовать механизм аутентификации из Раздела 6.3 для того чтобы дать пользователям возможность входить на сайт.
Если вы используете Git, было бы неплохо закоммитить внесенные изменения, если вы этого еще не сделали:
$ git add .
$ git commit -m "Make a basic User model (including secure passwords)"
Затем опять объединить их с мастер веткой:
$ git checkout master
$ git merge modeling-users
6.5 Упражнения
- Добавьте тесты для кода отвечающего за перевод email в нижний регистр из Листинга 6.23, как это показано в Листинге 6.31. Закомменирова строку
before_save
убедитесь что тесты из Листинга 6.31 тестируют правильные вещи. - Запустив набор тестов убедитесь что коллбэк
before_save
может быть написан способом показанным в Листинге 6.32. - Прочитайте Rails API введение для
ActiveRecord::Base
для того чтобы получить представление о ее возможностях. - Изучите Rails API введение для метода
validates
для того чтобы больше узнать о его возможностях и опциях. - Поиграйте пару часов с Rubular.
spec/models/user_spec.rb
require 'spec_helper'
describe User do
.
.
.
describe "email address with mixed case" do
let(:mixed_case_email) { "[email protected]" }
it "should be saved as all lower-case" do
@user.email = mixed_case_email
@user.save
@user.reload.email.should == mixed_case_email.downcase
end
end
.
.
.
end
before_save
. app/models/user.rb
class User < ActiveRecord::Base
attr_accessible :name, :email, :password, :password_confirmation
has_secure_password
before_save { email.downcase! }
.
.
.
end
- Имя происходит от “паттерна active record”, определенного и названного в Patterns of Enterprise Application Architecture Мартина Фаулера. ↑
- Произносится “ess-cue-ell”, хотя альтернативное произношение “sequel” также возможно. ↑
- Используя адреса электронной почты в качестве имени пользователя мы открываем теоретическую возможность связи с нашими пользователями в будущем. ↑
- Не волнуйтесь о том, как объект
t
делает это; красота уровней абстракции (the beauty of abstraction layers) состоит в том, что мы не должны этого знать. Мы можем просто доверить объектуt
делать его работу. ↑ - Официально произносится как “ess-cue-ell-ite”, тем не менее, вариант произношения “sequel-ite” также возможен. ↑
- На случай, если
"2011-12-05 00:57:46"
вызвало ваше любопытство - я не пишу это после полуночи; временнЫе метки записаны во Всемирном координированном времени (UTC), которое для многих практических целей является аналогом Среднего времени по Гринвичу. Из NIST Time and Frequency FAQ: Q: Почему UTC используется в качестве акронима к Coordinated Universal Time вместо CUT? A: В 1970 система Coordinated Universal Time была разработана международной консультативной группой технических экспертов в рамках International Telecommunication Union (ITU). ITU чувствовал, что было лучше определить единственное сокращение для использования на всех языках, чтобы минимизировать беспорядок. Так как единогласное соглашение не могло быть достигнуто при использовании английского порядка слов, CUT, или французского порядка слов, TUC, акроним, UTC был выбран в качестве компромисса. ↑ - Значение
user.updated_at
говорит вам о том что временнАя метка была в UTC. ↑ - Исключения и обработка исключений - несколько более продвинутые предметы Ruby и мы не сильно будем нуждаться в них в этой книге. Они важны, тем не менее, и я предлагаю узнать о них используя одну из книг, рекомендованных в Разделе 1.1.1. ↑
- Я опускаю выводы консольных команд когда они не особенно поучительны, например, результат
User.new
. ↑ - Обратите внимание, что в Таблице 6.1, “буква” на самом деле означает “строчную букву”, но
i
в конце regex обеспечивает нечувствительность к регистру. ↑ - Если вы считаете это столь же полезным, как я, призываю вас внести пожертвование чтобы вознаградить разработчика Michael Lovitt за его замечательную работу над Rubular. ↑
- Знаете ли вы, что
"Michael Hartl"@example.com
, с кавычками и пробелом в середине - является допустимым адресом электронной почты согласно стандарту? Невероятно, абсурдно. Если у вас нет адреса электронной почты, который содержит только буквы, номера, подчеркивания, и точки, то получите. N.B. regex в Листинге 6.17 также позволяет знаки "плюс", потому что Gmail (и возможно другие сервисы электронной почты) делает что-то полезное с ними: например, чтобы фильтровать email от example.com, вы можете использовать [email protected], который пойдет по Gmail адресу [email protected], позволив вам фильтровать по строке example. ↑ - Как было вкратце отмечено во введении в этот раздел, есть отдельная тестовая база данных,
db/test.sqlite3
для этой цели. ↑ - Конечно, мы могли только отредактировать файл миграции для таблицы
users
в Листинге 6.2, но для этого потребовалось бы откатить, а затем вновь накатить миграцию базы данных. Rails Way заключается в использовании миграции каждый раз, когда мы обнаруживаем, что нам необходимо изменить модель данных. ↑ - Непосредственные эксперименты со SQLite на моей системе и с PostgreSQL на Heroku показали что этот шаг фактически необходим. ↑
- https://railstutorial.ru/book?version=3.0 ↑