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

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

Главу 5 мы закончили созданием страницы-заглушки для регистрации пользователей (Раздел 5.4); в течение следующих четырех глав мы выполним обещание, неявное в этой начинающейся странице регистрации. Первый важный шаг это создание модели данных для пользователей нашего сайта, вместе со способом хранить эти данные. В Главе 7 мы дадим пользователям возможность регистрироваться на нашем сайте и создадим страницу профиля пользователя. Как только пример приложения сможет создавать новых пользователей, мы также позволим им входить и выходить (Глава 8) и в Главе 9 (Раздел 9.2.1) мы узнаем как защитить страницы от несанкцонированного доступа. Взятые вместе, материалы с Главы 6 по Главу 9 разрабатывают полную Rails систему входа и аутентификации. Как вы, возможно, знаете, для Rails существует множество готовых решений для аутентификации; Блок 6.1 поясняет почему разворачивание собственной системы является лучшей идеей.

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

___________________________________________

Блок 6.1.Прикручивание собственной системы аутентификации

Фактически всем веб-приложениям в настоящее время требуется какая либо система входа и аутентификации. Неудивительно, что у большинства веб-фреймворков есть множество вариантов реализации подобных систем, и Rails не исключение. Примеры систем аутентификации и авторизации включают в себя Clearance, Authlogic, Devise и CanCan (так же как не-Rails-специфичные решения, построенные на основе OpenID или OAuth). Резонный вопрос - почему мы должны изобретать велосипед. Почему бы просто не использовать готовое решение вместо того чтобы прикручивать свое?

С одной стороны, практика показывает, что аутентификация на множестве сайтов требует серьезной кастомизации и модификация стороннего продукта, это, обычно, даже большая работа чем написание собственной системы с нуля. К тому же, готовые решения это "черные ящики", с весьма загадочными внутренностями; а когда вы пишете свою собственную систему у вас гораздо больше шансов разобраться в ней. Кроме того, последние дополнения к Rails (Раздел 6.3) очень облегчили написание собственной системы аутентификации. Наконец, если вы все же когда-либо решите использовать стороннюю систему, вам будет гораздо проще в ней разобраться если вы прежде имели опыт написания собственной .

___________________________________________

(Первая строка здесь только для того чтобы удостовериться, что вы находитесь на master ветке, чтобы тема ветки modeling-users была основана на master ветке. Можно пропустить эту команду, если вы уже находитесь в master ветке.)

Модель User

Хотя конечная цель следующих трех глав это создание страницы регистрации для нашего сайта (ее набросок показан на рис. 6.1), в принятии регистрационной информации сейчас хорошего мало, так как нам в настоящий момент попросту негде ее хранить. Таким образом, первый шаг в регистрации пользователей должен создать структуру данных для получения и хранения их информации.

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

Рис. 6.1. Набросок страницы регистрации пользователей.

В Rails дефолтную структуру данных для модели данных называют, что достаточно естественно, модель (М. в MVC из Раздела 1.2.6). Дефолтное решение Rails для проблемы персистентности состоит в том, чтобы использовать базу данных для долгосрочного хранения данных и дефолтную библиотеку Active Record для взаимодействия с базой данных.1Имя происходит от "паттерна active record", определенного и названного в Patterns of Enterprise Application Architecture Мартина Фаулера. Active Record идет с массой методов для создания, хранения и поиска объектов данных, и все они не требуют использования языка структурированных запросов (SQL)2Произносится "ess-cue-ell", хотя альтернативное произношение "sequel" также возможно. применяемого реляционными базами данных. Кроме того, у Rails есть функции, называемые миграциями, которые позволяют писать определения данных на чистом Ruby, без необходимости изучать язык определения данных (DDL). Ка к результат, Rails почти полностью изолирует вас от деталей хранения данных. В этой книге, благодаря использованию SQLite для разработки и PostgreSQL (через Heroku) для развертывания (Раздел 1.4), мы проработали эту тему еще дальше, до точки, где нам едва ли когда-нибудь придется задумываться о том, как Rails хранит данные, даже для рабочих приложений.

Миграции базы данных

Можно вспомнить из Раздела 4.4.5 , что мы уже встречали, в созданном нами классе User объекты user с атрибутами name и email. Тот класс служил полезным примером, но он испытывал недостаток в критическом свойстве персистентности: когда мы создали объект User в консоли Rails, он исчез, как только мы вышли. Наша цель в этом Разделе состоит в том, чтобы создать модель для пользователей, которые не будут исчезать так легко.

Как и с классом User в Разделе 4.4.5, мы начнем с моделирования пользователя с двумя атрибутами: name и email, последний мы будем использовать в качестве уникального имени пользователя.3Как было отмечено в Разделе 6.2.4, регулярное выражение для email в Листинге 6.14 позволяет невалидные адреса электронной почты с последовательно расположенными точками, т.e., адреса вида "foo@bar..com". Добавьте этот адрес в список невалидных адресов в Листинге 6.13 для того чтобы получить провальный тест, а затем с помощью усложненного регулярного выражения показанного в Листинге 6.32 добейтесь прохождения этого теста. (Мы добавим атрибут пароля в Разделе 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.31 что мы создавали контроллер 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
Листинг 6.1. Генерация модели User.

(Обратите внимание, что, в отличие от множественного соглашения для имен контроллеров, названия моделей - в ед. числе: контроллер Users, но модель User.) Передавая дополнительные параметры name:string и email:string, мы говорим Rails о двух желаемых атрибутах, наряду с тем, какого типа эти атрибуты должны быть (в данном случае, string). Сравните это с включением имен действий в Листинге 3.4 и Листинге 5.31.

Одним из результатов generate команды в Листинге 6.1 является новый файл, названный migration. Миграции обеспечивают возможность постепенного изменения структуры базы данных, так, чтобы наша модель данных могла адаптироваться к изменяющимся требованиям. В случае модели User, миграция создается автоматически сценарием генерации модели; что создает таблицу users с двумя столбцами, name и email, как это показано в Листинге 6.2. (Мы увидим в Разделе 6.2.5 и еще раз в Разделе 6.3 как создавать миграцию с нуля.)

class CreateUsers < ActiveRecord::Migration
  def change
    create_table :users do |t|
      t.string :name
      t.string :email

      t.timestamps
    end
  end
end
Листинг 6.2. Миграция для модели User (создающая таблицу users). db/migrate/[timestamp]_create_users.rb

Обратите внимание: у названия файла миграции есть префикс в виде временнОй отметки основанной на времени генерации миграции. В первые дни миграций, названия файлов имели префиксы в виде увеличивающихся целых чисел, что приводило к конфликтам в командах разработчиков в случаях когда несколько программистов создавали миграции с совпадающими номерами. Использование временнЫх меток позволило комфортно избегать подобных коллизий.

Сама миграция представляет собой метод change определяющий изменения которые необходимо внести в базу данных. В случае Листинга 6.2, change использует Rails метод называемый create_table для создания таблицы в базе данных для хранения пользователей. Метод create_table принимает блок (Раздел 4.3.2) с одной переменной блока, в данном случае названной t (от "table"). Внутри блока метод create_table использует объект t для создания name и email столбцов в базе данных, оба с типом string.4Не волнуйтесь о том, как объект t делает это; красота уровней абстракции (the beauty of abstraction layers) состоит в том, что мы не должны этого знать. Мы можем просто доверить объекту t делать его работу. Здесь название таблицы во множественном числе (users) даже при том, что название модели в ед. числе (User), что отражает лингви стическое соглашение которому следует Rails: модель представляет единственного (отдельного) пользователя, тогда как таблица базы данных состоит из многих пользователей. Заключительная строка в блоке, t.timestamps, является специальной командой, которая создает два волшебных столбца, называемые created_at и updated_at, которые являются временнЫми отметками, которые автоматически записывают, когда данный пользователь создается и обновляется. (Мы увидим конкретные примеры волшебных столбцов в Разделе 6.1.3.) Полная модель данных, представленная этой миграцией, показана на рис. 6.2.

Модель данных 'пользователи', произведенная Листингом 6.2.

Рис. 6.2. Модель данных 'пользователи', произведенная Листингом 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 в качестве уникального идентификатора каждой строки.

SQLite Database Browser с нашей новой users таблицей.

Рис. 6.3. SQLite Database Browser с нашей новой users таблицей.

Большинство миграций являются обратимыми, а это означает что мы можем "migrate down" и переделать ее с помощью единственной Rake задачи, называемой db:rollback:

$ bundle exec rake db:rollback

(См. в Блоке 3.2 еще одну полезную для обращения миграций технику.) Под капотом этой Rake задачи происходит выполнение команды drop_table для удаления таблицы users из базы данных. Причина по которой это работает кроется в том, что метод change знает что drop_table это команда обратная create_table и это означает что способ отката миграции легко определим. В случае необратимых миграций, таких как удаление столбца из базы данных, необходимо определять отдельные up и down методы вместо единственного метода change. Почитайте о миграциях в (rus)Rails Guides дабы составить о них более полное представление.

Если вы откатывали базу данных, migrate up снова перед продолжением:

$ bundle exec rake db:migrate

Файл модели

Мы видели, как генерация модели 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).

class User < ActiveRecord::Base
end
Листинг 6.3. Совершенно новая модель User. app/models/user.rb

Вспомните из Раздела 4.4.2 что синтаксис class User < ActiveRecord::Base означает что класс User наследует от ActiveRecord::Base, таким образом, у модели User автоматически есть вся функциональность ActiveRecord::Base класса. Конечно, знание этого наследования не приносит пользы, если мы не знаем что содержит ActiveRecord::Base так что давайте начнем с конкретных примеров.

Создание объектов 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.

Вызванный без параметров, User.new возвращает объект с nil атрибутами. В Разделе 4.4.5 мы спроектировали пример класса User таким образом, чтобы он принимал инициализационный хэш для установки атрибутов объекта; такое решение было обусловлено библиотекой Active Record, которая позволяет инициализировать объекты тем же способом:

>> user = User.new(name: "Michael Hartl", email: "mhartl@example.com")
=> #<User id: nil, name: "Michael Hartl", email: "mhartl@example.com",
created_at: nil, updated_at: nil>

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

Если вы следили за development log, вы, возможно, заметили, что новые строки еще не обнаружились. Это связано с тем, что вызов User.new не касается базы данных; он просто создает новый Ruby объект в памяти. Чтобы сохранить объект user в базе данных, мы вызовем метод save на переменной user:

>> user.save
=> true

Метод save возвращает true если сохранение успешно выполнилось и false если сохранение не выполнено. (Сейчас все сохранения должны успешно выполняться; но в Разделе 6.2 мы увидим случаи, когда некоторые из них не сработают.) После сохранения в логе консоли должна появиться строка с командой SQL INSERT INTO "users". Из-за множества методов, предоставляемых Active Record, в этой книге нам не потребуется необработанный SQL и я буду опускать обсуждение команд SQL с этого момента. Но вы можете многому научиться, читая SQL соответствующий командам Active Record.

Вы, возможно, заметили что у нового объекта user были nil значения для атрибутов id и волшебных столбцов created_at и updated_at. Давайте посмотрим, изменило ли наше save что-нибудь:

>> user
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2013-03-11 00:57:46", updated_at: "2013-03-11 00:57:46">

Мы видим что id было присвоено значение 1, в то время как волшебным столбцам были присвоены текущие время и дата.5На случай, если "2013-03-11 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 был выбран в качестве компромисса. В настоящий момент, метки "создан" (created) и "обновлен" (updated) идентичны; мы увидим, что они могут отличаться в Разделе 6.1.5.

Как и с классом User в Разделе 4.4.5, экземпляры модели User предоставляют доступ к своим атрибутам, используя точку:6Значение user.updated_at говорит вам о том что временнАя метка была в UTC

>> user.name
=> "Michael Hartl"
>> user.email
=> "mhartl@example.com"
>> user.updated_at
=> Mon, 11 Mar 2013 00:57:46 UTC +00:00

Как мы увидим в Главе 7, часто бывает удобно создать и сохранить модель в два приема, как мы это сделали выше, но Active Record также позволяет вам объединить эти действия в один шаг с User.create:

>> User.create(name: "A Nother", email: "another@example.org")
#<User id: 2, name: "A Nother", email: "another@example.org", created_at:
"2013-03-11 01:05:24", updated_at: "2013-03-11 01:05:24">
>> foo = User.create(name: "Foo", email: "foo@bar.com")
#<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11
01:05:42", updated_at: "2013-03-11 01:05:42">

Обратите внимание: User.create, вместо того чтобы возвратить true или false, возвращает сам объект User который мы можем дополнительно присвоить переменной (такой как foo во второй команде выше).

Команда, обратная create это destroy:

>> foo.destroy
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11
01:05:42", updated_at: "2013-03-11 01:05:42">

Странно, destroy, как и create, возвращает рассматриваемый объект, хотя я не могу вспомнить что когда-либо использовал значение, возвращаемое destroy. Еще более странно то, что destroyенный объект все еще существует в памяти:

>> foo
=> #<User id: 3, name: "Foo", email: "foo@bar.com", created_at: "2013-03-11
01:05:42", updated_at: "2013-03-11 01:05:42">

Как мы узнаем, уничтожили ли мы в действительности объект? И как мы можем получить сохраненные и неуничтоженные объекты user из базы данных? Пора узнать, как использовать Active Record, для поиска объектов user.

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

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

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