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

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

Поиск объектов user

Active Record предоставляет несколько способов поиска объектов. Давайте используем их, для того, чтобы найти первого пользователя, которого мы создали, и чтобы проверить, что третий пользователь (foo) был уничтожен. Мы начнем с существующего пользователя:

>> User.find(1)
=> #<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 пользователя в 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.7Исключения и обработка исключений - несколько более продвинутые предметы Ruby и мы не сильно будем нуждаться в них в этой книге. Они важны, тем не менее, и я предлагаю узнать о них используя одну из книг, рекомендованных в Разделе 1.1.1.

В дополнение к универсальному find, Active Record также позволяет нам искать пользователей по определенным атрибутами:

>> User.find_by_email("mhartl@example.com")
=> #<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">

Метод find_by_email автоматически создается библиотекой Active Record на основе email атрибута в таблице users. (Как вы догадываетесь, Active Record также создает метод find_by_name.) Начиная с Rails 4.0, более предпочтительным способом для поиска атрибута является использование метода find_by с атрибутом передаваемым в виде хэша:

>> User.find_by(email: "mhartl@example.com")
=> #<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">

Поскольку мы будем использовать адреса электронной почты в качестве имен пользователей, этот вид find будет полезен когда мы узнаем как позволить пользователям регистрироваться на нашем сайте (Глава 7). Если вы беспокоитесь об эффективности find_by при большом количестве пользователей - ваше беспокойство вполне обоснованно, но вы немного забегаете вперед; мы обсудим эту проблему и ее решение в Разделе 6.2.5.

Мы закончим несколькими более общими способами поиска пользователей. Во-первых, first:

>> User.first
=> #<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">

стественно, first просто возвращает первого пользователя в базе данных. Есть также all:

>> User.all
=> [#<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">,
#<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">]

Не сюрприз, что all возвращает массив (Раздел 4.3.1) всех пользователей в базе данных.

Обновление объектов user

После создания объектов мы зачастую хотим их обновить. Есть два основных способа сделать это. Во-первых, мы можем присвоить атрибуты индивидуально, как мы это делали в Разделе 4.4.5:

>> 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">
>> user.email = "mhartl@example.net"
=> "mhartl@example.net"
>> user.save
=> true

Обратите внимание: заключительный шаг необходим, для того чтобы записать изменения в базу данных. Мы можем увидеть, что произойдет без сохранения, используя reload, которая перезагружает объекты, опираясь на информацию в базе данных:

>> user.email
=> "mhartl@example.net"
>> user.email = "foo@bar.com"
=> "foo@bar.com"
>> user.reload.email
=> "mhartl@example.net"

Теперь, когда мы обновили пользователя, выполнив user.save, волшебные столбцы отличаются, как и обещалось в Разделе 6.1.3:

>> user.created_at
=> "2013-03-11 00:57:46"
>> user.updated_at
=> "2013-03-11 01:37:32"

Второй основной способ обновления атрибутов заключается в использовании update_attributes:

>> user.update_attributes(name: "The Dude", email: "dude@abides.org")
=> true
>> user.name
=> "The Dude"
>> user.email
=> "dude@abides.org"

Update_attributes метод принимает хеш атрибутов и в случае успеха выполняет и обновление, и сохранение за один шаг (возвращающая true чтобы указать что сохранение произошло). Обратите внимание - если какая-нибудь из валидаций выдаст ошибку, что может случиться если, например, пароль был указан как обязательный для сохранения записи (как это реализовано в Разделе 6.3), вызов update_attributes провалится и обновления записи не произойдет. Если мы хотим обновить только один атрибут, то использование сингулярного update_attribute позволит обойти это ограничение:

>> user.update_attribute(:name, "The Dude")
=> true
>> user.name
=> "The Dude"

Валидации User

У модели User, которую мы создали в Разделе 6.1 теперь есть рабочие атрибуты name и email, но они абсолютно универсальны: любая строка (включая пустую) в настоящий момент допустима. И все же, имена и адреса электронной почты это нечто более определенное. Например, name не должно быть пробелом, email должен соответствовать определенному формату, характерному для адресов электронной почты. Кроме того, так как мы будем использовать адреса электронной почты в качестве уникальных имен пользователей при регистрации, мы не должны позволять дублироваться адресам электронной почты в базе данных.

Короче говоря, мы не должны позволить name и email быть просто любыми строками; мы должны реализовать определенные ограничения для их значений. Active Record позволяет нам налагать такие ограничения, с помощью validations. В этом разделе мы рассмотрим несколько из наиболее распространенных случаев, применив валидации для наличия, длины, формата и уникальности. В Разделе 6.3.4 мы добавим заключительную общепринятую валидацию - подтверждение. И мы увидим в Разделе 7.3 как валидации дают нам удобные сообщения об ошибках когда пользователи предоставляют данные которые нарушают их.

Начальные тесты для пользователей

Как и все прочие фичи этого примера приложения, мы добавим валидации модели User с помощью разработки через тестирование. Поскольку мы не передали флаг

--no-test-framework

при генерации модели User (в отличие, например, от Листинга 5.31), команда в Листинге 6.1 создала начальные спеки для тестирования пользователей, но в данном случае они практически пусты (Листинг 6.4).

require 'spec_helper'

describe User do
  pending "add some examples to (or delete) #{__FILE__}"
end
Листинг 6.4. Практически пустой дефолтный спек User. spec/models/user_spec.rb

Здесь просто используется метод pending для указания на то, что мы должны заполнить спек чем-нибудь полезным. Мы cможем увидеть результат его применения подготовив (пустую) тестовую базу данных и запустив спек модели User:

$ bundle exec rake db:migrate
$ bundle exec rake test:prepare
$ 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)

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

Это наша первая встреча с командой которая создает тестовую базу данных с корректной структурой:

$ bundle exec rake test:prepare

Это просто обеспечивает соответствие между моделью данных базы данных для разработки в db/development.sqlite3 и моделью данных тестовой базы данных в db/test.sqlite3.8Это подразумевает что вы используете SQLite локально. Этих файлов не будет если вы вместо SQLite используете Postgres как это рекомендовалось в одном из упражнений Главы 3. (Незапуск этой Rake задачи после миграции является частым источником недоразумений. К тому же, иногда тестовая база данных выходит из строя и требуется вернуть ее в исходное состояние. Если ваш набор тестов загадочным образом рухнул, попробуйте запустить rake test:prepare - возможно это решит проблему.)

Мы последуем совету дефолтного спека и заполним его небольшим количеством RSpec примеров, как это показано в Листинге 6.5.

require 'spec_helper'

describe User do

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

  subject { @user }

  it { should respond_to(:name) }
  it { should respond_to(:email) }
end
Листинг 6.5. Тестирование :name и :email атрибутов. spec/models/user_spec.rb

Блок before, который мы видели в Листинге 5.30), запускает код внутри блока перед каждым тестом, в данном случае, создавая новую переменную экземпляра @user с помощью User.new и валидного инициализационного хэша. Затем

subject { @user }

делает @user дефолтным cубъектом тестирования, как мы это видели прежде в контексте переменной page в Разделе 5.3.4.

Два теста в Листинге 6.5 тестируют на наличие name и email атрибутов:

it { should respond_to(:name) }
it { should respond_to(:email) }

Сами по себе эти тесты не особенно полезны, так как объект User у которого нет (например) атрибута name бросит исключение в блоке before. Но эти тесты проверяют что конструкции user.name и user.email являются валидными, в то время как блок before лишь тестирует атрибуты когда они передаются в виде хэша методу new. К тому же, тестирование атрибутов модели это полезная конвенция, так как она позволяет нам с первого взгляда увидеть методы на которые должна отвечать модель.

Методы respond_to неявно используют 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 кода

it "should respond to 'name'" do
  expect(@user).to respond_to(:name)
end

Благодаря subject { @user }, мы можем написать это в альтернативном однострочном стиле, впервые представленном в Разделе 5.3.4:

it { should respond_to(:name) }

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

Так как мы уже подготовили тестовую базу данных командой rake test:prepare, тесты должны пройти:

$ bundle exec rspec spec/
Вадим Обозин
Вадим Обозин

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

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