Agile Web Development with Rails 翻译(十三)
第八章 Task C: 创建购物车
现在我们已有了可显示我们所产品的分类目录列表,它可以很好地销售它们。我们客户很满意,所以我们一同决定这次要实现购物车功能。这包括了一些新概念,包括会话以及数据库表之间的父子关系,现在我们就可开始吧。
2006年4月17日更新
8.1 “会话”(session)
在开始之前,我们需要花费些时间来看看会话,Web应用程序和Rails。
当用户浏览我们的在线分类目录时,他(我们希望)选择要购买的产品。约定每个被选择的产品应该被添加到我们商店的虚拟商店购物车中。在有些时候,我们的顾客会需要很多东西,并且他会给我们的站点付款,为他购物车内的商品付款。
这意味着我们的应用程序将需要保持所有由顾客选购到购物车内的每个商品。这听起来很简单,除了一些微小的细节。在浏览器与应用程序之间的协议是无状态的—没有内建的记忆。每次,当你的应用程序接受来自浏览器的一个请求时,就如同是第一次一样。当你试图去记住你的顾客已经选择了什么产品时,就会麻烦一些。
对这个问题的大多数解决办法是在HTTP(它是无状态的)“头”上加上一些有状态的事物。应用程序内的某一层会试着对引入的请求,匹配它持有的本地“会话”数据部分。如果“会话”数据的特定部分会匹配来自特定浏览器的全部请求,我们会保持使用“会话”数据浏览器的顾客购下的所有东西的轨迹。
完成这个“会话”轨迹的基本机制是各式各样的。有时候,一个应用程序在每一页的格式化信息内编码“会话”信息。有时候在每个URL的尾部添加被编码的“会话”标识符(也称为URL重写操作)。有时候应用程序使用cookie。Rails使用基于cookie的途径。
一个cookie是一组由web应用程序传递给Web浏览器的有名字数据。浏览器在用户机器上存储本地cookie。随后,当浏览器给应用程序发送请求时,会附带cookie数据标记。应用程序使用cookie内的信息来匹配服务端内存储的带有“会话”信息的请求。这是对一个脏乱问题的丑陋的解决方式。幸运地,做为一个Rails程序员,你不必为这些低级的细节操心。(事实上,这就为什么在使用例子前,Rails应用程序要求浏览器必须打开cookie的原因。)
开发者不心担心任何协议和cookie,Rails会我们提供一个简单的抽象。在“控制器”内部,Rails维护着一个特殊的类似哈希表的集合,称为“会话”。你在对一个请求的处理期间,存储到这个哈希表内的任何key/value对,都将在同样浏览器的后续请求中有效。
在Depot应用程序中,我们想使用“会话”功能来存储顾客购物车内的产品信息。但是在这儿要小心一些—有些深层的问题可能会出现。
缺省情况下,Rails存储会话信息在服务端的一个文件内。如果你运行了一个单独的Rails服务器,就没有任何问题。但是想像一下,你的应用程序得到了普遍欢迎,超出你单独服务器的容量,并且需要运行在多个服务器中。来自于特定用户的第一个请求可能被一个分枝机器处理,但第二个请求可能被另一个分枝机器处理。存储在第一个分枝机器内的“会话”数据对第二个分枝机器来说无效的;用户会感到混乱,因为商品一会儿出现在它们的购物车中,一会儿却又消失了。
所以,最好的办法是确保“会话”信息被存储在应用程序外部的某处,这个地方可在多个应用程序需要处理的时候被共享。如果能有这样的外部存储,我们甚至可以弹回一个服务并且不会丢失任何会话信息。我们在440页的第二十二章谈论会话信息的设置。但现在,让我们假设Rails能处理所有这些情况。
现在,我们已艰难地看过了所理论。我们需要能够在它第一次需要时,指派个新的购物车对象给一个“会话”,并且在同样的“会话”内,在每次需要时都能找到这个购物车对象。我们可以通过在store的“控制器”内,创建一个“帮助方法”方法find_cart()来做到这些。下面是该方法的实现:
private
def find_cart
session[:cart] ||= Cart.new
end
这个方法很巧妙。它使用Ruby的条件赋值操作符,||=。如果“会话”已有相应于键:cart的值的话,那么就立即返回这个值。否则的话创建个新的购物车对象并赋值给这个“会话”。然后返回这个新的购物车。
我们让find_cart()方法成为private。这会阻止Rails在“控制器”上做为一个“动作”来设置它的值。
8.2 更多的表,更多的 “模型”
因此我们知道我们需要创建一些种类的购物车来持有顾客已经选择的东西,并且知道我们要在与“会话”关联的请求之间保持这个购物车。这个购物车保持商品项,一商品项就是一个产品及其数量的结合。例如,如果顾客购买了一本测试用书,购物车将持有数据库内带有一个数量和对这本书引用的商品项。我们与客户谈到这些,谁会提醒我们,如果产品的单价发生了变化,不会去修改原有订单的价格,所以,我们也需要在商品项中加入一个单价字段。
基于我们现在掌握的东西,我们可以通过向create.sql脚本中添加一个新的表定义来创建我们的line_items表。注意外键引用的连接是相应产品的商品项。
drop table if exists line_items;
create table line_items (
id int not null auto_increment,
product_id int not null,
quantity int not null default 0,
unit_price decimal(10,2) not null,
constraint fk_items_product foreign key (product_id) references products(id),
primary key (id)
);
记得把你的新定义加入到你的MySQL计划中。
depot> mysql depot_development <db/create.sql
我们需要回忆一下对这个新表来创建一个Rails “模型”。注意,这儿是名字映射是如何工作的:LineItem的类名字将被映射到基础表line_items。类名字混合大小写(每个单词以大写字母开头,彼此间没有空格)。表名字(像我们稍后会看到的,和变量名字及符号)是小写的,单词间用个下划线。(更多细节你可查看191页的14.1节。)
depot> ruby script/generate model LineItem
最后,我们必须告诉Rails有关商品项和products表之间的引用。你可能认为Rails会遍历数据库定义并发现些关联,但不是所有数据库引擎都支持外键的。[例如,流行的MySQL数据库没有在内部实现外键,除非你为交叉表指定一个特定的实现。]相反,我们必须明确地告诉Rails表之间存在的关联。在这个特殊情况中,我们通过告诉Rails商品项属于belongs_to()一个产品,来表示商品项和一个产品间的关联。我们在目录app/models/line_item.rb文件内直接指定。
class LineItem < ActiveRecord::Base
belongs_to :product
end
Rails使用一个“名字约定”来让它对外键如何在一个基础数据库内工作做出假设。让我们看看显示在图8.1内逻辑的“模型”,schema,和Rails类的结果。如果叫Child的“模型”属于Parent “模型”,则Rails假设表children有个列parent_id引用了parents表内的id列。[是的,Rails足够聪明知道叫Child的“模型”应该映射名为children的表。]在我们的line item “模型”内,属性关联意味着一个商品项内有个列product_id引用了products表内的id列。就像Rails内的大多数事情一样,如果假设它用的列名字不能与你特定的计划进行工作,你总是可以覆写它的。
然后,就是购物车本身。我们需要给它一个类,但是我们也需要一个购物车的数据库表吗?不需要,购物车只受顾客的“会话”约束,只要“会话”数据在我们的所有服务器上是有效的(当我们最终配置多服务器环境时),这或许足够好了。所以现在,我们假设购物车是个正常类并可看到生了什么。在购物车内部,我们要持有商品项(它符合lint_items表内的行),但我们不想保存这些商品项到数据库中,直到在结算时我们创建定单。