在我们新的 反馈网站 上最受欢迎的请求是轻松地在单个表单中管理多个模型的能力。感谢 Eloy Duran 提供的补丁,正好满足这一需求。但在将其引入 rails 2.3 之前,我们希望从各位那里获得更多反馈。Eloy 撰写了这个补丁的简介,因此一睹为快,然后将您的任何反馈添加到 Lighthouse 工作单 中。
在 Rails 中,创建适用于模型与其关联的单个表单一直有些棘手。去年夏天,:accessible 选项已提交,这是处理嵌套模型的统一解决方案的首次尝试。不过,这款功能在 2.2 版本发布之前被撤销,因为它仅在对象创建过程中支持嵌套模型。
由此引发了 核心邮件列表中的讨论,加上我们对批量分配的需求,最终形成了 针对工作单 #1202 的此补丁,现已开放审查。
由于这是一个非常大的补丁,我们非常欢迎您试用一下。请报告遇到的任何问题,或提供有关此补丁的总体反馈。我们特别有兴趣了解此解决方案在哪些应用程序中适用,这些应用程序允许用户通过表单删除嵌套记录。
下面,我将快速介绍此补丁的作用以及如何使用它,这样您就没有借口不试用一下了。
首先创建一个新应用程序
$ mkdir -p nested-models/vendor
$ cd nested-models/vendor
使用嵌套模型补丁向您的 Rails 分支添加供应商
$ git clone git://github.com/alloy/rails.git
$ cd rails
$ git checkout origin/normalized_nested_attributes
$ cd ../..
$ ruby vendor/rails/railties/bin/rails .
假设您有一个带有关联任务的项目模型
class Project < ActiveRecord::Base
has_many :tasks
validates_presence_of :name
end
class Task < ActiveRecord::Base
belongs_to :project
validates_presence_of :name
end
现在,请考虑使用以下表单,它允许您同时创建(或编辑)项目及其任务
<form>
<div>
<label for="project_name">Project:</label>
<input type="text" name="project_name" />
</div>
<p>
<label for="task_name_1">Task:</label>
<input type="text" name="task_name_1" />
<label for="task_delete_1">Remove:</label>
<input type="checkbox" name="task_delete_1" />
</p>
<p>
<label for="task_name_2">Task:</label>
<input type="text" name="task_name_2" />
<label for="task_delete_2">Remove:</label>
<input type="checkbox" name="task_delete_2" />
</p>
</form>
在打此补丁之前,您必须像这样编写模板
<% form_for @project do |project_form| %>
<div>
<%= project_form.label :name, 'Project name:' %>
<%= project_form.text_field :name %>
</div>
<% @project.tasks.each do |task| %>
<% new_or_existing = task.new_record? ? 'new' : 'existing' %>
<% prefix = "project[#{new_or_existing}_task_attributes][]" %>
<% fields_for prefix, task do |task_form| %>
<p>
<div>
<%= task_form.label :name, 'Task:' %>
<%= task_form.text_field :name %>
</div>
<% unless task.new_record? %>
<div>
<%= task_form.label :_delete, 'Remove:' %>
<%= task_form.check_box :_delete %>
</div>
<% end %>
</p>
<% end %>
<% end %>
<%= project_form.submit %>
<% end %>
控制器非常类似于您的普通 restful 控制器。但是,Project 模型需要知道如何处理嵌套属性
class Project < ActiveRecord::Base
after_update :save_tasks
def new_task_attributes=(task_attributes)
task_attributes.each do |attributes|
tasks.build(attributes)
end
end
def existing_task_attributes=(task_attributes)
tasks.reject(&:new_record?).each do |task|
attributes = task_attributes[task.id.to_s]
if attributes['_delete'] == '1'
tasks.delete(task)
else
task.attributes = attributes
end
end
end
private
def save_tasks
tasks.each do |task|
task.save(false)
end
end
validates_associated :tasks
end
上面的代码基于 Ryan Bates 的 complex-form-examples 应用程序 和 高级 Rails 配方 书籍。
首先,告诉 Project 模型接受其任务的嵌套属性
class Project < ActiveRecord::Base
has_many :tasks
accept_nested_attributes_for :tasks, :allow_destroy => true
end
然后,您可以编写以下模板
<% form_for @project do |project_form| %>
<div>
<%= project_form.label :name, 'Project name:' %>
<%= project_form.text_field :name %>
</div>
<!-- Here we call fields_for on the project_form builder instance.
The block is called for each member of the tasks collection. -->
<% project_form.fields_for :tasks do |task_form| %>
<p>
<div>
<%= task_form.label :name, 'Task:' %>
<%= task_form.text_field :name %>
</div>
<% unless task_form.object.new_record? %>
<div>
<%= task_form.label :_delete, 'Remove:' %>
<%= task_form.check_box :_delete %>
</div>
<% end %>
</p>
<% end %>
<% end %>
<%= project_form.submit %>
<% end %>
正如您所看到的,这样简洁多了,也更容易阅读。
当然,此示例的模板仅仅是短了一点点,但是,假设您拥有更多的嵌套模型,或者 Task 模型嵌入了自己的模型,那么您就很容易想象出两者之间的差别。
验证很管用,正如您所料;#valid?还会验证嵌套模型,#save(false)可以不验证就保存,等等。
唯一需要注意的是嵌套模型的所有错误消息都被复制到父错误对象中以供error_messages_for使用。如该缺陷单所述,未来可能会有所更改,但此更改不在此修补程序的范围内。
我们来看一个示例,在该示例中,Task 验证其:name属性
>> project = Project.first
=> #<Project id: 1, name: "Nested models patches", created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15", author_id: 1>
>> project.tasks
=> [#<Task id: 1, project_id: 1, name: "Write 'em", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 2, project_id: 1, name: "Test 'em", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 3, project_id: 1, name: "Create demo app", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 4, project_id: 1, name: "Scrutinize", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">]
>> project.tasks.second.name = ""
=> ""
>> project.valid?
=> false
>> project.errors
=> #<ActiveRecord::Errors:0x23e4b10 @errors={"tasks_name"=>["can't be blank"]}, @base=#<Project id: 1, name: "Nested models patches", …, author_id: 1>>
现在你可能会想当验证通过但无法保存时会如何保证数据的连贯性。考虑以下 Author 模型,我已操纵它以便在保存后引发异常
class Author < ActiveRecord::Base
has_many :projects
after_save :raise_exception
def raise_exception
raise 'Oh noes!'
end
end
在更新之前,这是 Project 数据
>> project = Project.first
=> #<Project id: 1, name: "Nested models patches", created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15", author_id: 1>
>> project.tasks
=> [#<Task id: 1, project_id: 1, name: "Write 'em", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 2, project_id: 1, name: "Test 'em", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 3, project_id: 1, name: "Create demo app", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">,
#<Task id: 4, project_id: 1, name: "Scrutinize", due_at: nil, created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">]
>> project.author
=> #<Author id: 1, name: "Eloy", created_at: "2009-01-22 11:17:15", updated_at: "2009-01-22 11:17:15">
现在让我们删除第一个 Task 并更改第二个 Task 的名称
>> project.tasks_attributes = { "1" => { "_delete" => "1" }, "2" => { "name" => "Who needs tests anyway?!" } }
=> {"1"=>{}, "2"=>{"name"=>"Who needs tests anyway?!"}}
>> project.tasks.first.marked_for_destruction?
=> true
>> project.tasks.forty_two
=> nil # Oops, I meant #second of course… ;)
>> project.tasks.second.name
=> "Who needs tests anyway?!"
最后,我们尝试保存 Project 实例
>> project.save
RuntimeError: Oh noes!
from /Users/eloy/code/complex-form-examples/app/models/author.rb:9:in `raise_exception_if_needed'
保存嵌套模型之一时引发了异常。现在我们来看看对数据发生了什么
SQL (0.1ms) BEGIN
Task Destroy (0.3ms) DELETE FROM `tasks` WHERE `id` = 1
Task Update (0.2ms) UPDATE `tasks` SET `updated_at` = '2009-01-22 11:22:23', `name` = 'Who needs tests anyway?!' WHERE `id` = 2
SQL (17.0ms) ROLLBACK
正如你所看到的,所有更改都被回滚。更新和删除标记为要销毁的记录都在同一个事务中进行。
与所有事务一样,你尝试保存的实例的属性不会被重置。这意味着在此事务失败之后,第一个任务仍标记为要销毁,第二个任务仍具有新的名称值。
此修补程序允许你创建深度自定义的嵌套模型表单。创建、保存和删除应在单个事务内部透明地进行。
请在你自己的应用程序上测试此修补程序,或查看 使用此修补程序的 Ryan complex-form-examples 的我的分支。
我们非常欢迎你的反馈。但请记住,我们不可能同时满足每个人的需要。请在应用修补程序之后作为单独的缺陷单提出其他功能或重大更改。我们的目的是首先修复此修补程序中的错误,以便我们所有人都能构建在稳固的基础之上。
最后,我要感谢 David Dollar 提供的原始:accessible实现,感谢 Fingertips(我工作的公司)赞助用于此修补程序的时间,感谢 Manfred Stienstra 对文档提供的广泛帮助,感谢 Lance Ivy、Michael Koziarski、Ryan Bates、Pratik Naik 和 Josh Susser 对一般性讨论做出的贡献(按无特定顺序排列)。