65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (1投票)

2013 年 1 月 24 日

CPOL

8分钟阅读

viewsIcon

15724

一个嵌套表单的演示。

背景

在过去的几个月里,我一直在做一个 Rails 3.2 项目。虽然学习新的技术栈(我职业生涯的大部分时间都在使用 .NET)非常有趣,但确实也遇到了一些困难。特别是,我花了过去几周时间来处理如何持久化一个具有父子关系的模型。大部分时间都花在了浏览互联网,寻找实现目标的方法和技巧。在互联网搜索过程中,我找不到任何一篇(或一系列文章)提供了关于如何持久化具有父/子关系模型的完整示例或教程。我的谷歌搜索能力可能不如其他开发者,但我确实很努力地搜索了。我找到的都是零散的片段(主要是在我的救星 Stackoverflow 上)。经过大量的试错(主要是后者),我终于成功了。所以我想把我的笔记整理成一系列文章。顺便说一句,这原本是一篇文章,但变得太长了。即使对我来说也是如此。

目标/宗旨

阅读很有趣,我读了很多。我倾向于通过实践来学习,所以只阅读只能让我完成一半。为此,我们将构建一个演示应用程序,最终解决以下问题:

  • 允许用户保存具有父/子关系的模型。
    • 该操作允许我们保存父对象以及插入/更新/删除与父对象关联的新子对象。
  • 处理保存具有多对多关系的模型(例如,一个用户可能属于多个安全组)。

应用程序

我看到的大多数演示应用程序都处理用户、客户、安全组之类的东西。这些都很好,但我认为围绕我喜欢的东西做一个演示应用程序会很有趣——《星球大战》!《星球大战》我最喜欢的方面是飞船,特别是战斗机。所以,让我们构建一个简单的数据库应用程序,它允许我们维护关于《星球大战》宇宙中战斗机的数据。演示应用程序背后的想法很简单:它是一个《星球大战》星际战斗机识别指南(尽管是一个非常简化的版本)。

最初,我们初始应用程序的概念非常简单:

  1. 飞船
  2. 飞船有零个或多个飞行员(即驾驶所查看飞船类型的角色/人物)。

例如:Garven Dreis、Wedge Antilles 和 Biggs Darklighter 都驾驶 T-65 X 翼战斗机。

此应用程序将允许用户创建/编辑/删除一个 Ship。在创建 Ship 的同时,用户可以选择创建与正在创建的 Ship 相关联的 Pilot 的记录。编辑现有 Ship 时,用户可以:

  1. 创建新的 Pilot
  2. 编辑现有的 Pilot
  3. 从列表中删除 Pilot

用户可以在保存之前执行上述任何数量的更改。当表单提交时,我们期望 Rails/ActiveRecord 会正确处理。它将:

  1. 销毁已删除的 Pilots
  2. 添加新创建的 Pilots
  3. 更新已更改的 Pilots
  4. 更新已更改的 Ship 属性。

注意:本系列文章假定您具备 Ruby 和 Rails 的基本知识。我强烈建议您学习 Rails 教程 — 这是一个非常棒的技术入门。

好的,让我们开始吧。

模型

首先,让我们看一下我们将用于此演示的领域模型。我们将保持简单,拥有一个直接的父子关系。

  • Ship这将是我们的“父”对象 — 它代表了《星球大战》宇宙中星际战斗机的一些基本信息。
  • Pilot: 这将是我们的“子”对象 — 它代表了一个被评定为驾驶星际战斗机的人。

这是我们将要使用的领域模型的图片(感谢 RubyMine!)。

the Ship and Pilot model

接下来,让我们看看我们领域模型中每个对象的代码。

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 选项执行的任何代码都应返回 truefalse。在我们的例子中,如果所有属性都为空,我们不想构建 Pilot 记录。这将防止 ActiveRecord 在我们的 Pilots 表中保存空白/空行。

最后,我们有 :allow_destroy 选项。此选项可以设置为 truefalse,它的作用基本上与其名称相符。如果我们有 :allow_destroy => true(就像我们的情况一样),ActiveRecord 可以删除那些 :_destroy 属性设置为 truePilots

您可以在 此处查看 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 助手。我们将添加一些屏幕,让我们的用户能够对 ShipsPilots 执行 CRUD 操作。

敬请关注…… 

© . All rights reserved.