Rails 3.2:嵌套表单演示,第一部分:所有飞船报告!





4.00/5 (1投票)
一个嵌套表单的演示。
背景
在过去的几个月里,我一直在做一个 Rails 3.2 项目。虽然学习新的技术栈(我职业生涯的大部分时间都在使用 .NET)非常有趣,但确实也遇到了一些困难。特别是,我花了过去几周时间来处理如何持久化一个具有父子关系的模型。大部分时间都花在了浏览互联网,寻找实现目标的方法和技巧。在互联网搜索过程中,我找不到任何一篇(或一系列文章)提供了关于如何持久化具有父/子关系模型的完整示例或教程。我的谷歌搜索能力可能不如其他开发者,但我确实很努力地搜索了。我找到的都是零散的片段(主要是在我的救星 Stackoverflow 上)。经过大量的试错(主要是后者),我终于成功了。所以我想把我的笔记整理成一系列文章。顺便说一句,这原本是一篇文章,但变得太长了。即使对我来说也是如此。
目标/宗旨
阅读很有趣,我读了很多。我倾向于通过实践来学习,所以只阅读只能让我完成一半。为此,我们将构建一个演示应用程序,最终解决以下问题:
- 允许用户保存具有父/子关系的模型。
- 该操作允许我们保存父对象以及插入/更新/删除与父对象关联的新子对象。
- 处理保存具有多对多关系的模型(例如,一个用户可能属于多个安全组)。
应用程序
我看到的大多数演示应用程序都处理用户、客户、安全组之类的东西。这些都很好,但我认为围绕我喜欢的东西做一个演示应用程序会很有趣——《星球大战》!《星球大战》我最喜欢的方面是飞船,特别是战斗机。所以,让我们构建一个简单的数据库应用程序,它允许我们维护关于《星球大战》宇宙中战斗机的数据。演示应用程序背后的想法很简单:它是一个《星球大战》星际战斗机识别指南(尽管是一个非常简化的版本)。
最初,我们初始应用程序的概念非常简单:
- 有飞船。
- 飞船有零个或多个飞行员(即驾驶所查看飞船类型的角色/人物)。
例如:Garven Dreis、Wedge Antilles 和 Biggs Darklighter 都驾驶 T-65 X 翼战斗机。
此应用程序将允许用户创建/编辑/删除一个 Ship
。在创建 Ship
的同时,用户可以选择创建与正在创建的 Ship
相关联的 Pilot
的记录。编辑现有 Ship
时,用户可以:
- 创建新的
Pilot
。 - 编辑现有的
Pilot
。 - 从列表中删除
Pilot
。
用户可以在保存之前执行上述任何数量的更改。当表单提交时,我们期望 Rails/ActiveRecord 会正确处理。它将:
- 销毁已删除的
Pilots
。 - 添加新创建的
Pilots
。 - 更新已更改的
Pilots
。 - 更新已更改的
Ship
属性。
注意:本系列文章假定您具备 Ruby 和 Rails 的基本知识。我强烈建议您学习 Rails 教程 — 这是一个非常棒的技术入门。
好的,让我们开始吧。
模型
首先,让我们看一下我们将用于此演示的领域模型。我们将保持简单,拥有一个直接的父子关系。
Ship
:这将是我们的“父”对象 — 它代表了《星球大战》宇宙中星际战斗机的一些基本信息。Pilot
: 这将是我们的“子”对象 — 它代表了一个被评定为驾驶星际战斗机的人。
这是我们将要使用的领域模型的图片(感谢 RubyMine!)。
接下来,让我们看看我们领域模型中每个对象的代码。
app/models/ship.rb
class Ship < ActiveRecord::Base attr_accessible :armament, :crew, :has_astromech, :name, :speed attr_accessible :pilots_attributes has_many :pilots accepts_nested_attributes_for :pilots, :reject_if => lambda { |attrs| attrs.all? { |key, value| value.blank? } }, :allow_destroy => true #I find your lack of validation disturbing... end
我想提请您注意 attr_accessible :pilots_attributes
— 这将在以后变得很重要。
app/models/pilot.rb
class Pilot < ActiveRecord::Base belongs_to :ship attr_accessible :call_sign, :first_name, :last_name, :ship_id #I find your lack of validation disturbing... end
accepts_nested_attributes_for 方法
根据 文档,accepts_nested_attributes_for
方法“为指定的关联定义属性写入器。”这到底是什么意思?这意味着我们的 Ship
模型可以接受其关联的任何 Pilots
的属性并更新它们。要使用 accepts_nested_attributes_for
,我们将其指向我们的一个关联。目前,我们只有一个关联 — has_many :pilots
— 所以选择很简单。有了 accepts_nested_attributes_for :pilots
,我们就可以:
- 将新的
Pilots
添加到我们的Ship
。 - 更新我们
Ship
的现有Pilots
。 - 有一个特殊的属性(
_destroy
),它允许我们标记特定的Pilots
以便从我们的Ship
中删除(稍后将详细介绍)。
accepts_nested_attributes_for 如何工作
据我所知,幕后发生的事情是这样的:当您将 accepts_nested_attributes_for
方法添加到模型时,也会在您的模型中添加一个写入器。该写入器的命名方式如下:[您的关联名称]_attributes
。在我们的例子中,由于我们有一个 accepts_nested_attributes_for :pilots
方法,因此写入器的名称将是 pilots_attributes
(请注意 Pilot
的复数形式)。
accepts_nested_attributes_for 选项
请注意,我们的 Ship
模型还设置了一个 attr_accessible :pilots_attributes
访问器。这允许我们将 Pilot
数据轻松地传递给作为 accepts_nested_attributes_for
实现结果而创建的 pilots_attributes
写入器。将 pilots_attributes
包含在我们的 attr_accessible
列表中后,我们就不会遇到可怕的“无法批量分配”错误。
注意:让您的 accepts_nested_attributes_for
写入器可用于批量分配可能不适合您实际应用程序。在将模型的属性添加到 attr_accessible
列表时,请仔细考虑。
接下来,我们有 :reject_if
选项。此选项允许我们指定一个方法(使用符号或匿名方法)来确定是否应该构建 Pilot
记录。:reject_if
选项执行的任何代码都应返回 true
或 false
。在我们的例子中,如果所有属性都为空,我们不想构建 Pilot
记录。这将防止 ActiveRecord 在我们的 Pilots
表中保存空白/空行。
最后,我们有 :allow_destroy
选项。此选项可以设置为 true
或 false
,它的作用基本上与其名称相符。如果我们有 :allow_destroy => true
(就像我们的情况一样),ActiveRecord 可以删除那些 :_destroy
属性设置为 true
的 Pilots
。
您可以在 此处查看 accepts_nested_attributes_for
方法的文档和可用选项。
UI 中的 POST 请求是什么样的
我知道我跳得有点快,因为我们还没有讨论 UI。但是,我认为了解嵌套对象(即我们的 Pilots
)的数据是如何通过网络发送的会很有益。在观察了几次 POST 请求后,很容易看出我们上面在模型上所做的所有设置都在做什么。
场景 1 — 用户创建一个没有飞行员的新飞船
如果用户只创建了一个 Ship
而没有创建 Pilots
(即,在 ships_controller
上调用了 ships#create
方法),POST 中包含的数据将如下所示(取自 Firebug 并稍作美化)。
&ship[name]=TIE+Fighter &ship[crew]=1 &ship[has_astromech]=0 &ship[speed]=100 &ship[armament]=2+laser+cannons
如上所示,我们只传递了 Ship
对象 的属性。控制器将接收这些参数并构建一个新的 Ship
对象。这是当控制器将我们的新 Ship
保存到数据库时执行的 SQL。
INSERT INTO "ships" ("armament", "created_at", "crew", "has_astromech", "name", "speed", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?) [["armament", "2 laser cannons"], ["created_at", Tue, 15 Jan 2013 19:04:13 UTC +00:00], ["crew", 1], ["has_astromech", false], ["name", "TIE Fighter"], ["speed", 100], ["updated_at", Tue, 15 Jan 2013 19:04:13 UTC +00:00]]
场景 2 — 用户创建了一个带有两个飞行员的新飞船
现在我们的用户变得雄心勃勃了,但由于我们在模型上进行的设置,我们已经准备好了。在这种情况下,用户创建了一个新的 Ship
,并关联了两个 Pilots
。下面显示了一个创建带有两个 Pilots
的新 Ship
的 POST 请求。
&ship[name]=TIE+Fighter &ship[crew]=1 &ship[has_astromech]=0 &ship[speed]=100 &ship[armament]=2+laser+cannons &ship[pilots_attributes][1358277455305][first_name]=Cive &ship[pilots_attributes][1358277455305][last_name]=Rashon &ship[pilots_attributes][1358277455305][call_sign]=Howlrunner &ship[pilots_attributes][1358277455305][_destroy]=false &ship[pilots_attributes][1358277658761][first_name]=Dodson &ship[pilots_attributes][1358277658761][last_name]=Makraven &ship[pilots_attributes][1358277658761][call_sign]=Night+Beast &ship[pilots_attributes][1358277658761][_destroy]=false
让我们看看
pilots_attributes
参数 — 它们对应于我们的 :pilots_attributes
访问器。每组 pilots_attributes
都有四个属性,由一种“唯一 ID”分组(我们将在以后的文章中看到如何生成唯一 ID)。在我们的控制器中,当我们调用 Ship.new(params[:ship])
时,ActiveRecord 可以在 ships#create
方法中构建与 Ship
对应的 Pilots
。这是因为我们在 Ship
模型中声明了 attr_accessible :pilots_attributes
。我们还可以看到特殊的 _destroy
属性,在这种情况下都设置为 false
。这意味着我们想要将这些 Pilots
添加到我们的数据库。
为完整起见,这里是 ships_controller
中的 create
方法。
app/controllers/ships_controller.rb
def create @ship = Ship.new(params[:ship]) if @ship.save redirect_to(ships_path, :notice => "The #{ @ship.name } ship has been saved successfully.") else render(:new, :error => @ship.errors) end end
同样,这是 ActiveRecord 生成的 SQL,用于保存我们的 Ship
及其关联的 Pilots
。
(0.1ms) begin transaction
SQL (0.4ms) INSERT INTO "ships" ("armament", "created_at", "crew",
"has_astromech", "name", "speed", "updated_at") VALUES (?, ?, ?, ?, ?,
?, ?) [["armament", "2 laser cannons"], ["created_at", Tue, 15 Jan 2013
19:21:33 UTC +00:00], ["crew", 1], ["has_astromech", false], ["name",
"TIE Fighter"], ["speed", 100], ["updated_at", Tue, 15 Jan 2013 19:21:33
UTC +00:00]]
SQL (0.7ms) INSERT INTO "pilots" ("call_sign",
"created_at", "first_name", "last_name", "ship_id",
"updated_at") VALUES (?, ?, ?, ?, ?, ?) [["call_sign", "Howlrunner"],
["created_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00], ["first_name",
"Cive"], ["last_name", "Rashon"], ["ship_id", 8],
["updated_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00]]
SQL (0.3ms) INSERT INTO "pilots" ("call_sign", "created_at",
"first_name", "last_name", "ship_id",
"updated_at") VALUES (?, ?, ?, ?, ?, ?) [["call_sign", "Night Beast"],
["created_at", Tue, 15 Jan 2013 19:21:33 UTC +00:00], ["first_name",
"Dodson"], ["last_name", "Makraven"], ["ship_id", 8], ["updated_at",
Tue, 15 Jan 2013 19:21:33 UTC +00:00]]
(2.2ms) commit transaction
总结:看看它有多大!
我们才刚刚触及《星际战斗机识别指南》的表面,但我们已经详细地研究了如何:
- 在我们的模型中建立父/子关系。
- 设置我们的“父”模型以接受任何“子”对象的数据,从而允许我们一次性保存“父”及其所有“子”。
- 我们还运用了原力,展望了 UI 以及用户提交表单时 UI 将发送给控制器的 A. 当用户提交表单时,UI 将发送数据。这种对未来的窥视揭示了 Rails 在构建/保存具有父/子关系的模型时的一些内部机制。
在下一部分中,我们将仔细研究《星际战斗机识别指南》的控制器和 UI 助手。我们将添加一些屏幕,让我们的用户能够对 Ships
和 Pilots
执行 CRUD 操作。
敬请关注……