- 编写订单表相关的迁移文件并使用Rails提供的命令创建订单表;
- 编写订单模型的验证;
- 构建商铺模型和订单模型的关系;
- 编写订单模型的单元测试,完成相关测试。
我们已经开发完了用户和商铺以及商品模块,终于可以开始购物了,这也就是我们今天要开发的模块:订单。
开发前我们还是要明确订单模块的功能以及订单模块和其它模块之间的关系,特别是模型之间关系的确立!
我们可以先从功能的梳理开始。
- 用户进入店铺 --> 浏览商品 --> 选中商品 --> 用户下单;
- 用户查看自己的订单列表;
- 用户查看某个订单的详情。
当然我们这个需求非常的简单,如果要完成整个订单闭环,我们还有很多需求需要完成,但这里我们先完成这些。
基于以上需求和我们日常经验,我们可以分析出:
- 用户和订单是有关系的:一个用户可以多次下单,一个订单只属于一个用户,所以用户和订单是一对多的关系;
- 订单和商品之间是有关系的:一个订单可以包含多件商品,一种商品可以被多个订单包含,所以订单和商品是多对多的关系,这种多对多的关系,我们这里采用中间表的形式关联;
- 用户和商品之间的联系是通过订单来关联的,并没有直接形成关系。
所以我们需要两张表:订单主表和订单详情表,其中订单详情表也用做订单和商品之间模型的关联,而订单主表就存储所属用户的信息以及订单的统计信息。
- 订单存储用户信息,所以需要
标题
字段; - 订单需要有价格,所以需要
价格
字段; - 订单需要有上下架的状态,所以需要
上架状态
字段; - 订单是属于某个商铺的,需要
商铺ID
字段来标记; - 每个订单都应该由唯一的id,Rails已经为我们提供了默认的id字段,并且是主键,所以我们在这里不用自定义,直接使用默认即可;
- Rails还会自动帮我们维护两个字段:created_at, updated_at 。
字段 | 类型 | 长度 | 注释 | null | 默认值 |
---|---|---|---|---|---|
id | unsigned integer | 11 | 主键id,自动增长 | 否 | 0 |
user_id | int | 11 | 用户id | 否 | 0 |
price_total | int | 11 | 订单总价价格:精确到分 | 否 | 0 |
created_at | timestamp | --- | 创建时间 | 是 | 当前时间 |
updated_at | timestamp | --- | 修改时间 | 是 | 当前时间 |
- 添加和修改订单:price_total字段信息必须提供
- 添加和修改订单:price_total字段信息大于等于0
- 要关联订单,所以需要
订单id
字段; - 要关联商品,所以需要
商品id
字段; - 要记录订单购买的数量,所以需要
数量
字段; - Rails还会自动帮我们维护两个字段:created_at, updated_at 。
字段 | 类型 | 长度 | 注释 | null | 默认值 |
---|---|---|---|---|---|
order_id | int | 11 | 订单id | 否 | 0 |
product_id | int | 11 | 商品id | 否 | 0 |
quantity | int | 11 | 商品数量 | 否 | 0 |
created_at | timestamp | --- | 创建时间 | 是 | 当前时间 |
updated_at | timestamp | --- | 修改时间 | 是 | 当前时间 |
$ git checkout -b chapter09
$ rails generate model order user_id:integer price_total:integer
Running via Spring preloader in process 6566
invoke active_record
create db/migrate/20210521124134_create_orders.rb
create app/models/order.rb
invoke test_unit
create test/models/order_test.rb
create test/fixtures/orders.yml
可以看到命令行帮我们生成了 db/migrate/20210521124134_create_orders.rb
订单模型的迁移文件。
还帮我们生成了 app/models/order.rb
订单模型文件。
db/migrate/20210521124134_create_orders.rb
class CreateOrders < ActiveRecord::Migration[6.1]
def change
create_table :orders do |t|
t.integer :user_id, null: false, default:0
t.integer :price_total, null: false, default:0
t.timestamps
end
end
end
我们这里创建了orders表。
$ rails db:migrate
== 20210521124134 CreateOrders: migrating =====================================
-- create_table(:orders)
-> 0.0289s
== 20210521124134 CreateOrders: migrated (0.0290s) ============================
执行结果显示创建了 orders 表。
思路分析
我们需要做的验证:
- price_total:不能为空,整数,必须大于等于0。
app/models/order.rb
class Order < ApplicationRecord
belongs_to :user
validates :price_total, presence: true, numericality: { greater_than_or_equal_to: 0}
end
$ rails c
Running via Spring preloader in process 6723
2.7.2 :001 > o = Order.new({user_id:1, price_total:-1})
=> #<Order id: nil, user_id: 1, price_total: -1, created_at: nil, updated_at: nil>
2.7.2 :002 > o.valid?
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 LIMIT 1
=> false
2.7.2 :003 > o.errors.messages
=> {:price_total=>["must be greater than or equal to 0"]}
就像前面我们已经分析的:用户和订单是一对多的关系。并且我们应该知道当用户被删除,所属的订单也应该被删除。
# app/models/user.rb
class User < ApplicationRecord
# ......
has_many :orders, dependent: :destroy
# ......
end
end
# app/models/order.rb
class Order < ApplicationRecord
belongs_to :user
# ... ...
end
我们使用Rails内置的 Minitest 测试框架来编写以及测试我们的应用。
在这里需要测试两种场景:
- 成功的场景
- 使用全部合法的参数(
合法的user_id,合法的price_total
)创建订单,断言:通过验证
- 使用全部合法的参数(
- 失败的场景
- 使用
非法的price_total,合法的user_id
创建订单,断言:未通过验证
- 使用
在使用Rails的命令创建模型order时,Rails还帮我们自动创建了测试文件已经与模型对应的测试用预定义数据文件:
test/fixtures/orders.yml
我们可以修改这个文件的内容如下:
one:
user: one
price_total: 1
two:
user: two
price_total: 1
这里定义了两个订单:订单one
和 订单 two
, 然后我们就可以在测试文件中通过 orders(:one)
来获取第一个订单的对象, orders(:two)
来获取第二个订单的对象信息了。
测试的编写我们首先要确定文件路径:test/models/order_test.rb
,这个文件也是Rails帮我们自动创建的!现在开始编写相关测试吧!
-
使用全部合法的参数(
合法的user_id,合法的price_total
)创建订单,断言:通过验证test/models/order_test.rb
class orderTest < ActiveSupport::TestCase setup do @order = orders(:one) @user = users(:one) end # 使用合法参数 test 'valid: order with all valid things' do order = Order.new(user_id:@user.id, price_total:1) assert order.valid? end #... end
上面的测试首先定义
setup
方法创建一些公用数据。 -
使用
非法的price_total,合法的user_id
创建订单,断言:未通过验证test/models/order_test.rb
test 'invalid: order with invalid price_total' do order = Order.new(user_id:@user.id, price_total:-1) assert_not order.valid? end
$ rails test
... ...
Finished in 2.319223s, 19.8342 runs/s, 29.7513 assertions/s.
46 runs, 69 assertions, 0 failures, 0 errors, 0 skips
我们这里顺利通过测试。
关于上面的测试,我建议一个一个完成,不要都写完才测试!
$ git add . && git commit -m "Generate orders"
接下来我们开发订单详情有关内容。
$ rails generate model placement order_id:integer product_id:integer quantity:integer
Running via Spring preloader in process 2304
invoke active_record
create db/migrate/20210522011606_create_placements.rb
create app/models/placement.rb
invoke test_unit
create test/models/placement_test.rb
create test/fixtures/placements.yml
可以看到命令行帮我们生成了 db/migrate/20210522011606_create_placements.rb
订单详情模型的迁移文件。
还帮我们生成了 app/models/placement.rb
订单详情模型文件。
db/migrate/20210521124134_create_placements.rb
class CreatePlacements < ActiveRecord::Migration[6.1]
def change
create_table :placements do |t|
t.integer :order_id, null: false, default:0
t.integer :product_id, null: false, default:0
t.integer :quantity, null: false, default:0
t.timestamps
end
add_index :placements, [:order_id, :product_id]
add_index :placements, [:product_id, :order_id]
end
end
我们这里创建了placements表。
$ rails db:migrate
== 20210522011606 CreatePlacements: migrating =================================
-- create_table(:placements)
-> 0.0173s
-- add_index(:placements, [:order_id, :product_id])
-> 0.0106s
-- add_index(:placements, [:product_id, :order_id])
-> 0.0093s
== 20210522011606 CreatePlacements: migrated (0.0379s) ========================
执行结果显示创建了 placements 表。
思路分析
在这里我们使用Rails的规则,既然placement作为中间表关联多对多模型对象,我们分别让其中的字段 belongs_to 两张表。
app/models/placement.rb
class Placement < ApplicationRecord
belongs_to :order
belongs_to :product, inverse_of: :placements
end
app/models/order.rb
class Order < ApplicationRecord
# ... ...
has_many :placements, dependent: :destroy
has_many :products, through: :placements
# ... ...
end
app/models/product.rb
class Product < ApplicationRecord
# ... ...
has_many :placements, dependent: :destroy
has_many :orders, through: :placements
# ... ...
end
$ rails c
Running via Spring preloader in process 4334
Loading development environment (Rails 6.1.3.2)
2.7.2 :001 > u = User.first
User Load (0.4ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` ASC LIMIT 1
2.7.2 :002 > new_order = u.orders.create(price_total:0)
Order Create (0.6ms) INSERT INTO `orders` (`user_id`, `created_at`, `updated_at`) VALUES (1, '2021-10-22 02:20:15.476568', '2021-10-22 02:20:15.476568')
TRANSACTION (1.6ms) COMMIT
=> #<Order id: 3, user_id: 1, price_total: 0, created_at: "2021-10-22 02:20:15.476568000 +0000", updated_at: "2021-05-22 02:20:15.476568000 +0000">
2.7.2 :003 > p = Product.first
=> #<Product id: 1, title: "123", price: 1, published: 1, shop_id: 1, created_at: "2021-10-20 11:53:59.663300000 +0000", updated_at: "2021-10-20 11:53:59.663300000 +0000">
2.7.2 :004 > new_placements = new_order.placements.create(product_id:p.id, quantity:5)
TRANSACTION (0.2ms) BEGIN
Product Load (0.3ms) SELECT `products`.* FROM `products` WHERE `products`.`id` = 1 LIMIT 1
Placement Create (0.6ms) INSERT INTO `placements` (`order_id`, `product_id`, `quantity`, `created_at`, `updated_at`) VALUES (3, 1, 5, '2021-05-22 02:20:53.684609', '2021-10-22 02:20:
TRANSACTION (2.4ms) COMMIT
2.7.2 :005 > new_order.price_total = new_placements.quantity * p.price
=> 5
2.7.2 :006 > new_order.save
TRANSACTION (0.2ms) BEGIN
TRANSACTION (5.0ms) COMMIT
=> true
2.7.2 :007 > new_order
=> #<Order id: 3, user_id: 1, price_total: 5, created_at: "2021-10-22 02:20:15.476568000 +0000", updated_at: "2021-10-22 02:21:14.398478000 +0000">
Placement Load (0.5ms) SELECT `placements`.* FROM `placements` WHERE `placements`.`order_id` = 3 /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Associations::CollectionProxy [#<Placement id: 1, order_id: 3, product_id: 1, quantity: 5, created_at: "2021-10-22 02:20:53.684609000 +0000", updated_at: "2021-10-22 02:20:53.684609000 +0000">]>
2.7.2 :08 > new_order.products
Product Load (0.8ms) SELECT `products`.* FROM `products` INNER JOIN `placements` ON `products`.`id` = `placements`.`product_id` WHERE `placements`.`order_id` = 3 /* loading for inspe
=> #<ActiveRecord::Associations::CollectionProxy [#<Product id: 1, title: "123", price: 1, published: 1, shop_id: 1, created_at: "2021-10-20 11:53:59.663300000 +0000", updated_at: "2021-10-20 11:53:59.663300000 +0000">]>
2.7.2 :09 > p2 = Product.find(2)
Product Load (0.5ms) SELECT `products`.* FROM `products` WHERE `products`.`id` = 2 LIMIT 1
2.7.2 :010 > new_placement2 = new_order.placements.create(product_id:p2.id, quantity:3)
TRANSACTION (0.2ms) BEGIN
Placement Create (0.5ms) INSERT INTO `placements` (`order_id`, `product_id`, `quantity`, `created_at`, `updated_at`) VALUES (3, 2, 3, '2021-05-22 02:25:24.690102', '2021-05-22 02:25:24.690102')
=> #<Placement id: 2, order_id: 3, product_id: 2, quantity: 3, created_at: "2021-10-22 02:25:24.690102000 +0000", updated_at: "2021-10-22 02:25:24.690102000 +0000">
2.7.2 :011 > new_order.price_total = new_order.price_total + new_placement2.quantity * p2.price
=> 8
2.7.2 :012 > new_order.save
TRANSACTION (0.2ms) BEGIN
Order Update (0.6ms) UPDATE `orders` SET `orders`.`price_total` = 8, `orders`.`updated_at` = '2021-10-22 02:25:49.706885' WHERE `orders`.`id` = 3
TRANSACTION (1.8ms) COMMIT
2.7.2 :013 > new_order.placements
Placement Load (0.5ms) SELECT `placements`.* FROM `placements` WHERE `placements`.`order_id` = 3 /* loading for inspect */ LIMIT 11 02:20:53.684609000 +0000">, #<Placement id: 2, order_id: 3, product_id: 2, quantity: 3, created_at: "2021-10-22 02:25:24.690102000 +0000", updated_at: "2021-10-22 02:25:24.690102000 +0000">]>
2.7.2 :014 > new_order.products
Product Load (0.4ms) SELECT `products`.* FROM `products` INNER JOIN `placements` ON `products`.`id` = `placements`.`product_id` WHERE `placements`.`order_id` = 3 /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Associations::CollectionProxy [#<Product id: 1, title: "123", price: 1, published: 1, shop_id: 1, created_at: "2021-10-20 11:53:59.663300000 +0000", updated_at: "2021-10-20 11:53:59.663300000 +0000">, #<Product id: 2, title: "firtst test", price: 1, published: 1, shop_id: 1, created_at: "2021-10-21 01:44:48.450398000 +0000", updated_at: "2021-10-21 01:44:48.450398000 +0000">]>
2.7.2 :015 > u.orders
Order Load (0.3ms) SELECT `orders`.* FROM `orders` WHERE `orders`.`user_id` = 1 /* loading for inspect */ LIMIT 11
=> #<ActiveRecord::Associations::CollectionProxy [#<Order id: 1, user_id: 1, price_total: 100, created_at: "2021-10-22 01:56:37.357925000 +0000", updated_at: "2021-10-22 01:56:37.357925000 +0000">, #<Order id: 2, user_id: 1, price_total: 100, created_at: "2021-10-22 02:00:14.620024000 +0000", updated_at: "2021-10-22 02:00:14.620024000 +0000">, #<Order id: 3, user_id: 1, price_total: 8, created_at: "2021-10-22 02:20:15.476568000 +0000", updated_at: "2021-10-22 02:25:49.706885000 +0000">]>
2.7.2 :016 > u.orders.count
(0.7ms) SELECT COUNT(*) FROM `orders` WHERE `orders`.`user_id` = 1
=> 3
2.7.2 :017 > Placement.count
(18.2ms) SELECT COUNT(*) FROM `placements`
=> 2
2.7.2 :018 > new_order.destroy
TRANSACTION (0.3ms) BEGIN
Placement Load (0.4ms) SELECT `placements`.* FROM `placements` WHERE `placements`.`order_id` = 3
Placement Destroy (0.4ms) DELETE FROM `placements` WHERE `placements`.`id` = 1
Placement Destroy (0.6ms) DELETE FROM `placements` WHERE `placements`.`id` = 2
Order Destroy (0.6ms) DELETE FROM `orders` WHERE `orders`.`id` = 3
TRANSACTION (1.5ms) COMMIT
=> #<Order id: 3, user_id: 1, price_total: 8, created_at: "2021-10-22 02:20:15.476568000 +0000", updated_at: "2021-10-22 02:25:49.706885000 +0000">
2.7.2 :019 > Placement.count
(0.5ms) SELECT COUNT(*) FROM `placements`
=> 0
2.7.2 :020 > u.orders.count
(0.4ms) SELECT COUNT(*) FROM `orders` WHERE `orders`.`user_id` = 1
=> 2
虽然测试简单,但是我们能看到订单和商品以及用户之间的数据都可以正确的添加,删除,查询等。
$ git add . && git commit -m "set placements model "
我们完成了订单模型以及模型验证并完成了模型的相关测试,下节课我们将开发订单控制器相关功能!一定要跟上脚步,认真练习!!