《Agile Web Development with Rails》抄书笔记系列
“《Agile Web Development with Rails》抄书笔记系列”目录
虽然上一节,我们创建了个一个初步在购物车,也通过了功能性验证。但是,这个购物车还有很多不完善的地方,比如,如果顾客添加了多个同一件商品,那么我们就需要重组这个购物车。这一节,我们将从这方面着手,来完善这个购物车。
D呱呱
关于这节内容的代码:
- 这节开始之前:https://github.com/diguage/depot/tree/v-09.3
- 这节完成之后:https://github.com/diguage/depot/tree/v-10.1
解决上面提到的这个问题,其实很简单只需要给 line_items表增加一个表示数量的列即可,我们将这一列命名为quantity,数据类型为整形。这时就需要修改数据库的表结构。我们在前面用到了这样的相应指令(内部实现是一个脚本),那么我们可以使用类型的命令来完成这项工作。命令如下:
1 | rails generate migration add_quantity_to_line_items quantity :integer |
这个指令的意思是向某个表增加一到多列,列的名称和数据类型从这个指令的最后一部分参数来获取。与add_XXX_to_TABLE向对应的,Rails还提供了remove_XXX_from_TABLE方法,这两个用于向表TABLE中添加或者删除列。经过D瓜哥的多次测试后发现,这里的XXX只是一个名字或者说标识符,并不一定和表中的列相同;但是TABLE必须和相应的表名相同,否则就会报错。另外,这两个参数后必须跟相应的列名和数据类型才行,否则不产生任何作用。
一般情况下,我们向购物车中添加物品,默认是添加一件。所以,我们来修改迁移脚本,来设置表中列的默认值为1。打开%Depot%/db/migrate/20130316075458_add_quantity_to_line_items.rb文件,将其按照以下代码来修改:
1 | class AddQuantityToLineItems < ActiveRecord::Migration |
3 | add_column :line_items , :quantity , :integer , default: 1 |
这时,执行迁移命令,来完成迁移:
执行完成后,检查一下line_items表是不是多了一列?
修改完数据库表结构后,我们就需要在Cart中增加一个方法add_product(),来实现添加商品的功能:如果购物车中没有这件商品,则添加一件商品到购物车中;如果购物车中已有这件商品,则增加商品数量。打开%Depot%/app/models/cart.rb文件,增加如下方法:
1 | def add_product(product_id) |
2 | current_item = line_items.find_by_product_id(product_id) |
4 | current_item.quantity += 1 |
6 | current_item = line_items.build(product_id: product_id) |
也许大家会纳闷,我们并没有定义第二行中出现的find_by_product_id方法,但是为什么却可以使用呢?Active Record注意到这里有一个以find_by开头,以表中某列结尾的方法,则它会动态定义这样一个方法,添加到我们的类中。关于这块的内容,以后还会再深入讲解。
既然我们在Model中添加了相应的方法,则Controller可以使用Model的对象直接调用这个方法。所以,我们也需要对Controller做一些稍微的改动。打开%Depot%/app/controllers/line_items_controller.rb方法,根据下面的代码来做修改
03 | product = Product.find(params[ :product_id ]) |
04 | @line_item = @cart .add_product(product.id) |
06 | respond_to do |format| |
08 | format.html { redirect_to @line_item .cart, |
09 | notice: 'Line item was successfully created.' } |
10 | format.json { render json: @line_item , status: :created , location: @line_item } |
12 | format.html { render action: "new" } |
13 | format.json { render json: @line_item .errors, status: :unprocessable_entity } |
然后,我们来修改一下购物车的展示页面,把每种商品的数量展示出来。打开%Depot%/app/views/carts/show.html.erb文件,修改如下:
02 | < p id = "notice" ><%= notice %></ p > |
05 | < h2 >Your Pragmatic Cart</ h2 > |
08 | <% @cart.line_items.each do |item| %> |
09 | < li ><%= item.quantity %>×<%= item.product.title %></ li > |
这时,大家打开http://127.0.0.1:3000/,打开添加按钮,向购物车中添加一些东西,看看效果。有木有发现,如果以前添加过多个相同的商品,则同一件商品会有多个显示。看来,我们有必要对以前的数据做一次迁移。执行如下命令,添加一个数据迁移脚本:
1 | rails generate migration combine_items_in_cart |
打开产生的迁移脚本%Depot%/db/migrate/20110711000005_combine_items_in_cart.rb(由于时间戳的原因,您看到的文件名和我列出的并不一定完全一致),对其修改如下:
03 | Cart.all. each do |cart| |
05 | sums = cart.line_items.group( :product_id ).sum( :quantity ) |
06 | sums. each do |product_id, quantity| |
09 | cart.line_items.where(product_id: product_id).delete_all |
12 | cart.line_items.create(product_id: product_id, quantity: quantity) |
这时,执行迁移指令:
是不是报错了?
01 | F :\depot>rake db :migrate |
02 | == CombineItemsInCart: migrating ============================================= |
04 | An error has occurred, this and all later migrations canceled: |
06 | Can't mass-assign protected attributes: quantity |
08 | F :/depot/db/migrate/20130316082624_combine_items_in_cart.rb: 13 :in `block ( 2 levels) in up' |
09 | F :/depot/db/migrate/20130316082624_combine_items_in_cart.rb: 7 :in ` each ' |
10 | F :/depot/db/migrate/20130316082624_combine_items_in_cart.rb: 7 :in `block in up' |
11 | F :/depot/db/migrate/20130316082624_combine_items_in_cart.rb: 4 :in ` each ' |
12 | F :/depot/db/migrate/20130316082624_combine_items_in_cart.rb: 4 :in `up' |
14 | Tasks: TOP => db :migrate |
15 | (See full trace by running task with --trace) |
解决方法是,在%Depot%/app/models/line_item.rb文件中增加如下一行:
1 | attr_accessible :quantity |
D呱呱
关于这个问题,D瓜哥在上一节“《Agile Web Development with Rails》抄书笔记(10):创建购物车”中已经解释过了,这里就不再赘述了。
这时,再运行迁移指令:
应该就OK了。然后刷新购物车页面或者从首页添加商品然后调到购物车页面,相同的商品就在一列中显示出来了。
另外,还要给大家提醒一点,迁移的一个重要原则是每一步都必须可逆的。所以,上一步的合并也不例外。为了实现上一步合并的反响操作,我们就必须实现down()方法,查询出line_items表中quantity>1的记录,然后逐个将其插入到line_items表中;然后再把刚才查出来的记录删除掉。打开文件%Depot%/db/migrate/20130316082624_combine_items_in_cart.rb,修改代码如下:
03 | LineItem.where( "quantity>1" ). each do |line_item| |
05 | line_item.quantity.times do |
06 | LineItem.create cart_id: line_item.cart_id, |
07 | product_id: line_item.product_id, quantity: 1 |
然后执行如下命令来实现”回退”:
由于我们就是为了实现合并,所以这条指令就不实际执行了。
到这里,我们这节需要讲解的东西都已经说完了。给大家点悬念:大家觉得目前这个购物车的实现是否尽善尽美?是否还有可以改进的地方?这一点,到下一节来揭晓。
D呱呱
使用脚本添加列,但是还没运行迁移指令时,报错:
NoMethodError in Carts#show
Showing F:/depot/app/views/carts/show.html.erb where line #9 raised:
undefined method `quantity' for #<LineItem:0x3308490>
Extracted source (around line #9):
08 | <% @cart.line_items.each do |item| %> |
09 | < li ><%= item.quantity %>×<%= item.product.title %></ li > |