Модели ActiveRecord в Rails уже выполняют тяжелую работу, связанную с доступом к базе данных и связями моделей, но, немного поработав, они могут делать больше автоматически. Давайте узнаем, как!
Шаг 1 — Создайте приложение Base Rails
Эта идея работает для любого вида проекта ActiveRecord; однако, поскольку Rails является наиболее распространенным, мы будем использовать его для нашего примера приложения. В приложении, которое мы будем использовать, есть много пользователей , каждый из которых может выполнять ряд действий над проектами .
Если вы никогда ранее не создавали приложение Rails, то сначала прочтите этот учебник или программу . В противном случае запустите старую консоль и введите rails new example_app
чтобы создать приложение, а затем измените каталоги на новое приложение с помощью cd example_app
.
Шаг 2 — Создайте свои модели и отношения
Сначала мы генерируем пользователя, который будет владеть:
1
|
rails generate scaffold User name:text email:string password_hash:text
|
Скорее всего, в реальном проекте у нас было бы еще несколько полей, но пока это подойдет. Давайте теперь сгенерируем модель нашего проекта:
1
|
rails generate scaffold Project name:text started_at:datetime started_by_id:integer completed_at:datetime completed_by_id:integer
|
Затем мы редактируем созданный файл project.rb
чтобы описать отношения между пользователями и проектами:
1
2
3
4
|
class Project < ActiveRecord::Base
belongs_to :starter, :class_name =>»User», :foreign_key =>»started_by_id»
belongs_to :completer, :class_name =>»User», :foreign_key =>»completed_by_id»
end
|
и обратная связь в user.rb
:
1
2
3
4
|
class User < ActiveRecord::Base
has_many :started_projects, :foreign_key =>»started_by_id»
has_many :completed_projects, :foreign_key =>»completed_by_id»
end
|
Затем выполните быстрый rake db:migrate
, и мы готовы начать осваивать эти модели. Если бы только установление отношений с моделями было так просто в реальном мире! Теперь, если вы когда-либо использовали фреймворк Rails, вы, вероятно, ничего не узнали … пока!
Шаг 3 — Искусственные атрибуты круче, чем искусственная кожа
Первое, что мы собираемся сделать, это использовать некоторые автоматически генерирующие поля. Вы заметили, что когда мы создавали модель, мы создали хэш пароля, а не поле пароля. Мы собираемся создать поддельный атрибут для пароля, который преобразует его в хеш, если он присутствует.
Итак, в вашей модели мы добавим определение для этого нового поля пароля.
1
2
3
4
5
6
7
|
def password={new_password)
write_attribute(:password_hash, SHA1::hexdigest(new_password))
end
def password
«»
end
|
Мы храним только хеш против пользователя, поэтому мы не будем выдавать пароли без особых усилий.
Второй метод означает, что мы возвращаем что-то для использования формами.
Мы также должны убедиться, что у нас загружена библиотека шифрования Sha1; добавьте require 'sha1'
в файл application.rb
после строки 40: config.filter_parameters += [:password]
.
Поскольку мы изменили приложение на уровне конфигурации, перезагрузите его с помощью быстрого touch tmp/restart.txt
в вашей консоли.
Теперь давайте изменим форму по умолчанию, чтобы использовать ее вместо password_hash
. Откройте _form.html.erb
в _form.html.erb
app / models / users:
1
2
3
4
|
<div class=»field»>
<%= f.label :password_hash %><br />
<%= f.text_area :password_hash %>
</div>
|
становится
1
2
3
4
|
<div>
<%= f.label :password %><br/>
<%= f.text_field :password %>
</div>
|
Мы сделаем это поле действительным паролем, когда будем довольны.
Теперь загрузите http://localhost/users
и поиграйте с добавлением пользователей. Это должно выглядеть как на картинке ниже; отлично, не правда ли!
Подожди, что это? Он перезаписывает ваш хэш пароля каждый раз, когда вы редактируете пользователя? Давайте это исправим.
user.rb
откройте user.rb
и измените его следующим образом:
1
|
write_attribute(:password_hash, SHA1::hexdigest(new_password)) if new_password.present?
|
Таким образом, только когда вы вводите пароль, поле обновляется.
Шаг 4 — Автоматическая точность данных гарантирует или ваши деньги обратно
Последний раздел был посвящен изменению данных, которые получает ваша модель, но как насчет добавления дополнительной информации, основанной на уже известных фактах, без необходимости их указания? Давайте посмотрим на это с моделью проекта. Начните с просмотра http: // localhost / projects.
Быстро внесите следующие изменения.
*app/controllers/projects_controler.rb* line 24
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
# GET /projects/new
# GET /projects/new.json
def new
@project = Project.new
@users = [«—«,nil] + User.all.collect { |u|
respond_to do |format|
format.html # new.html.erb
format.json { render :json =>@project }
end
end
# GET /projects/1/edit
def edit
@project = Project.find(params[:id])
@users = [«—«,nil] + User.all.collect { |u|
end
|
*app/views/projects/_form.html.erb* line 24
1
|
<%= f.select :started_by_id, @users %>
|
*app/views/projects/_form.html.erb* line 24
1
|
<%= f.select :completed_by , @users%>
|
В рамках MVC роли четко определены. Модели представляют данные. Представления отображают данные. Контроллеры получают данные и передают их в представление.
Кому нравится заполнять поля даты / времени?
Теперь у нас есть полнофункциональная форма, но мне start_at
что мне нужно вручную установить время start_at
. Я бы хотел, чтобы он был установлен, когда я назначаю пользователя started_by
. Мы могли бы поместить его в контроллер, однако, если вы когда-либо слышали фразу «толстые модели, тощие контроллеры», вы знаете, что это делает плохой код. Если мы сделаем это в модели, она будет работать везде, где мы установили стартер или завершитель. Давайте сделаем это.
Сначала отредактируйте app/models/project.rb
и добавьте следующий метод:
1
2
3
4
5
6
7
|
def started_by=(user)
if(user.present?)
user = user.id if user.class == User
write_attribute(:started_by_id,user)
write_attribute(:started_at,Time.now)
end
end
|
Этот код гарантирует, что что-то действительно было передано. Затем, если это пользователь, он извлекает свой идентификатор и, наконец, записывает как пользователя *, так и * время, когда это произошло — святой дым! Давайте добавим то же самое для поля completed_by
.
1
2
3
4
5
6
7
|
def completed_by=(user)
if(user.present?)
user = user.id if user.class == User
write_attribute(:completed_by_id,user)
write_attribute(:started_at,Time.now)
end
end
|
Теперь отредактируйте вид формы, чтобы у нас не было этих временных интервалов. В app/views/projects/_form.html.erb
удалите строки 26-29 и 18-21.
Откройте http://localhost/projects
и попробуйте!
Найдите умышленную ошибку
Whoooops! Кто-то (я возьму тепло, потому что это мой код) вырезал и :started_at
, и забыл изменить метод :started_at
на :completed_at
во втором методе атрибутов (по большей части идентичном) (подсказка). Не важно, поменяй это и все пойдет … верно?
Шаг 5 — Помогите своему будущему Я, сделав дополнения проще
Таким образом, кроме небольшой путаницы, я думаю, что мы проделали довольно хорошую работу, но это ускользнуло, и код вокруг меня немного беспокоит. Почему? Что ж, давайте подумаем
- Это копирование и вставка дублирования: СУХОЙ (не повторяйся) — это принцип, которому нужно следовать.
- Что делать, если кто-то хочет добавить еще
somethingd_at
somethingd_by
authorised_at
иsomethingd_at
somethingd_by
в наш проект, как, скажем,authorised_at
иauthorised_by
> - Я могу представить, что довольно много таких полей будут добавлены.
И вот, вот идет заостренный босс и просит, {drumroll}, authorised_at / by field и предложил_at / by field! Прямо тогда; давайте тогда подготовим эти вырезанные и вставленные пальцы … или есть лучший способ?
Страшное искусство метапрограммирования!
Это верно! Святой Грааль; страшные вещи, о которых тебя предупреждали мамы. Это кажется сложным, но на самом деле может быть довольно просто — особенно то, что мы собираемся попробовать. Мы собираемся взять массив имен этапов, которые у нас есть, а затем автоматически построить эти методы на лету. В восторге? Отлично.
Конечно, нам нужно будет добавить поля; так что давайте добавим rails generate migration additional_workflow_stages
миграции, rails generate migration additional_workflow_stages
db/migrate/TODAYSTIMESTAMP_additional_workflow_stages.rb
и добавим эти поля во вновь созданный db/migrate/TODAYSTIMESTAMP_additional_workflow_stages.rb
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
class AdditionalWorkflowStages < ActiveRecord::Migration
def up
add_column :projects, :authorised_by_id, :integer
add_column :projects, :authorised_at, :timestamp
add_column :projects, :suggested_by_id, :integer
add_column :projects, :suggested_at, :timestamp
end
def down
remove_column :projects, :authorised_by_id
remove_column :projects, :authorised_at
remove_column :projects, :suggested_by_id
remove_column :projects, :suggested_at
end
end
|
Перенесите базу данных с помощью rake db:migrate
и замените класс проектов на:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class Project < ActiveRecord::Base
# belongs_to :starter, :class_name =>»User»
# def started_by=(user)
# if(user.present?)
# user = user.id if user.class == User
# write_attribute(:started_by_id,user)
# write_attribute(:started_at,Time.now)
# end
# end
#
# def started_by
# read_attribute(:completed_by_id)
# end
end
|
Я оставил там started_by
чтобы вы могли видеть, как код был раньше.
1
2
3
|
[:starte,:complete,:authorise,:suggeste].each do |arg|
..MORE..
end
|
Хороший и нежный — проходит через имена (ish) методов, которые мы хотим создать:
1
2
3
4
5
6
7
8
|
[:starte,:complete,:authorise,:suggeste].each do |arg|
attr_by = «#{arg}d_by_id».to_sym
attr_at = «#{arg}d_at».to_sym
object_method_name = «#{arg}r».to_sym
…MORE…
end
|
Для каждого из этих имен мы определяем два атрибута модели, которые мы устанавливаем, например, started_by_id
и started_at
и название ассоциации, например, starter
1
2
3
4
5
6
7
8
9
|
[:starte,:complete,:authorise,:suggeste].each do |arg|
attr_by = «#{arg}d_by_id».to_sym
attr_at = «#{arg}d_at».to_sym
object_method_name = «#{arg}r».to_sym
belongs_to object_method_name, :class_name =>»User», :foreign_key =>attr_by
end
|
Это кажется довольно знакомым. На самом деле это уже что-то вроде метапрограммирования в Rails, которое определяет кучу методов.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
[:starte,:complete,:authorise,:suggeste].each do |arg|
attr_by = «#{arg}d_by_id».to_sym
attr_at = «#{arg}d_at».to_sym
object_method_name = «#{arg}r».to_sym
belongs_to object_method_name, :class_name =>»User», :foreign_key =>attr_by
get_method_name = «#{arg}d_by».to_sym
define_method(get_method_name) { read_attribute(attr_by) }
end
|
Хорошо, теперь мы подошли к некоторому реальному метапрограммированию, которое вычисляет имя ‘get method’ — например, started_by
, а затем создает метод, так же, как мы делаем, когда пишем def method
, но в другой форме.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
[:starte,:complete,:authorise,:suggeste].each do |arg|
attr_by = «#{arg}d_by_id».to_sym
attr_at = «#{arg}d_at».to_sym
object_method_name = «#{arg}r».to_sym
belongs_to object_method_name, :class_name =>»User», :foreign_key =>attr_by
get_method_name = «#{arg}d_by».to_sym
define_method(get_method_name) { read_attribute(attr_by) }
set_method_name = «#{arg}d_by=».to_sym
define_method(set_method_name) do |user|
if user.present?
user = user.id if user.class == User
write_attribute(attr_by,user)
write_attribute(attr_at,Time.now)
end
end
end
|
Немного сложнее сейчас. Мы делаем то же, что и раньше, но это имя метода set . Мы определяем этот метод, используя define(method_name) do |param| end
define(method_name) do |param| end
, а не def method_name=(param)
.
Это было не так плохо, правда?
Попробуйте это в форме
Посмотрим, сможем ли мы редактировать проекты, как раньше. Оказывается, мы можем! Итак, мы добавим дополнительные поля в форму, и, эй, Presto!
app/views/project/_form.html.erb
строка 20
1
2
3
4
5
6
7
8
9
|
<div class=»field»>
<%= f.label :suggested_by %><br/>
<%= f.select :suggested_by, @users %>
</div>
<div class=»field»>
<%= f.label :authorised_by %><br/>
<%= f.select :authorised_by, @users %>
</div>
|
И к представлению шоу … так что мы можем видеть это работает.
* app / views-project / show.html.erb * строка 8
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
<p>
<b>Suggested at:</b> <%= @project.suggested_at %>
</p>
<p>
<b>Suggested by:</b> <%= @project.suggested_by_id %>
</p>
<p>
<b>Authorised at:</b> <%= @project.authorised_at %>
</p>
<p>
<b>Authorised by:</b> <%= @project.authorised_by_id %>
</p>
|
Еще раз поиграйте с http://localhost/projects
, и вы увидите, что у нас есть победитель! Не нужно бояться, если кто-то попросит еще один шаг рабочего процесса; просто добавьте миграцию для базы данных и поместите ее в массив методов … и она будет создана. Время отдохнуть? Возможно, но у меня есть еще две вещи, чтобы отметить.
Шаг 6 — Автоматизация автоматизации
Этот набор методов мне кажется весьма полезным. Можем ли мы сделать больше с этим?
Во-первых, давайте сделаем список имен методов постоянным, чтобы мы могли получить к нему доступ извне.
1
2
3
|
WORKFLOW_METHODS = [:starte,:complete,:authorise,:suggeste]
WORKFLOW_METHODS.each do |arg|….
|
Теперь мы можем использовать их для автоматического создания форм и представлений. Откройте _form.html.erb
для проектов, и давайте попробуем это, заменив строки 19 -37 фрагментом ниже:
1
2
3
4
5
6
|
<% Project::WORKFLOW_METHODS.each do |workflow|
<div class=»field»>
<%= f.label «#{workflow}d_by» %><br/>
<%= f.select «#{workflow}d_by», @users %>
</div>
<% end %>
|
Но настоящая магия в app/views-project/show.html.erb
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<p id=»notice»><%= notice %></p>
<p>
<b>Name:</b>: <%= @project.name %>
</p>
<% Project::WORKFLOW_METHODS.each do |workflow|
at_method = «#{workflow}d_at»
by_method = «#{workflow}d_by_id»
who_method = «#{workflow}r»
%>
<p>
<b><%= at_method.humanize %>:</b>: <%= @project.send(at_method) %>
</p>
<p>
<b><%= who_method.humanize %>:</b>: <%= @project.send(who_method) %>
</p>
<p>
<b><%= by_method.humanize %>:</b>: <%= @project.send(by_method) %>
</p>
<% end %>
<%= link_to ‘Edit’, edit_project_path(@project) %> |
<%= link_to ‘Back’, projects_path %>
|
Это должно быть довольно ясно, хотя, если вы не знакомы с send()
, это еще один способ вызова метода. Таким образом, object.send("name_of_method")
совпадает с object.name_of_method
.
Финальный спринт
Мы почти закончили, но я заметил две ошибки: одна — форматирование, а другая — более серьезная.
Во-первых, когда я просматриваю проект, весь метод показывает ужасный вывод объекта Ruby. Вместо добавления метода в конец, как это
1
|
@project.send(who_method).name
|
Давайте to_s
User
чтобы иметь метод to_s
. Сохраните все в модели, если можете, и добавьте это в начало user.rb
, а также проделайте то же самое для project.rb
. Всегда имеет смысл иметь представление по умолчанию для модели в виде строки:
1
2
3
|
def to_s
name
end
|
Чувствует себя немного приземленными методами написания лёгкого пути сейчас, а? Нет? Во всяком случае, на более серьезные вещи.
Актуальная ошибка
Когда мы обновляем проект, потому что мы отправляем все этапы рабочего процесса, которые были назначены ранее, все наши метки времени смешиваются. К счастью, поскольку весь наш код находится в одном месте, одно изменение исправит их все.
01
02
03
04
05
06
07
08
09
10
11
12
|
define_method(set_method_name) do |user|
if user.present?
user = user.id if user.class == User
# ADDITION HERE
# This ensures it’s changed from the stored value before setting it
if read_attribute(attr_by).to_i != user.to_i
write_attribute(attr_by,user)
write_attribute(attr_at,Time.now)
end
end
end
|
Вывод
Что мы узнали?
- Добавление функциональности в модель может серьезно улучшить весь остальной код
- Метапрограммирование не невозможно
- Предположение, что проект может быть зарегистрирован
- Написание умных в первую очередь означает меньше работы, а потом
- Никто не любит вырезать, вставлять и редактировать, и это вызывает ошибки
- Умные модели сексуальны во всех сферах жизни
Большое спасибо за чтение, и дайте мне знать, если у вас есть какие-либо вопросы.