Статьи

Интеллектуальные модели ActiveRecord

Модели ActiveRecord в Rails уже выполняют тяжелую работу, связанную с доступом к базе данных и связями моделей, но, немного поработав, они могут делать больше автоматически. Давайте узнаем, как!


Эта идея работает для любого вида проекта ActiveRecord; однако, поскольку Rails является наиболее распространенным, мы будем использовать его для нашего примера приложения. В приложении, которое мы будем использовать, есть много пользователей , каждый из которых может выполнять ряд действий над проектами .

Если вы никогда ранее не создавали приложение Rails, то сначала прочтите этот учебник или программу . В противном случае запустите старую консоль и введите rails new example_app чтобы создать приложение, а затем измените каталоги на новое приложение с помощью cd example_app .


Сначала мы генерируем пользователя, который будет владеть:

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, вы, вероятно, ничего не узнали … пока!


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

Итак, в вашей модели мы добавим определение для этого нового поля пароля.

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 Form

Подожди, что это? Он перезаписывает ваш хэш пароля каждый раз, когда вы редактируете пользователя? Давайте это исправим.

user.rb откройте user.rb и измените его следующим образом:

1
write_attribute(:password_hash, SHA1::hexdigest(new_password)) if new_password.present?

Таким образом, только когда вы вводите пароль, поле обновляется.


Последний раздел был посвящен изменению данных, которые получает ваша модель, но как насчет добавления дополнительной информации, основанной на уже известных фактах, без необходимости их указания? Давайте посмотрим на это с моделью проекта. Начните с просмотра 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 во втором методе атрибутов (по большей части идентичном) (подсказка). Не важно, поменяй это и все пойдет … верно?


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

  • Это копирование и вставка дублирования: СУХОЙ (не повторяйся) — это принцип, которому нужно следовать.
  • Что делать, если кто-то хочет добавить еще 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 , и вы увидите, что у нас есть победитель! Не нужно бояться, если кто-то попросит еще один шаг рабочего процесса; просто добавьте миграцию для базы данных и поместите ее в массив методов … и она будет создана. Время отдохнуть? Возможно, но у меня есть еще две вещи, чтобы отметить.


Этот набор методов мне кажется весьма полезным. Можем ли мы сделать больше с этим?

Во-первых, давайте сделаем список имен методов постоянным, чтобы мы могли получить к нему доступ извне.

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

Что мы узнали?

  • Добавление функциональности в модель может серьезно улучшить весь остальной код
  • Метапрограммирование не невозможно
  • Предположение, что проект может быть зарегистрирован
  • Написание умных в первую очередь означает меньше работы, а потом
  • Никто не любит вырезать, вставлять и редактировать, и это вызывает ошибки
  • Умные модели сексуальны во всех сферах жизни

Большое спасибо за чтение, и дайте мне знать, если у вас есть какие-либо вопросы.