- 编写商品表相关的迁移文件并使用Rails提供的命令创建商品表;
- 编写商品模型的验证;
- 构建商铺模型和商品模型的关系;
- 编写商品模型的单元测试,完成相关测试。
我们已经开发完了用户和商铺模块,今天我们来开发商品模块。开发前我们需要明确我们已经开发的用户模型和商铺模型与今天要开发的商品模型之间的关系。
用户有3个基本角色:管理员,商铺拥有者和普通的用户。商铺拥有者拥有商铺,在我们的系统中一个用户最多只能拥有一个商铺,而商品是属于商铺的,一个商铺可以拥有多件商品,而一件商品只能属于一个商铺,所以商铺和商铺是多对一的关系。至于用户模型,其实任何人都是可以购买商品的,所以实际上用户要购买商品可以购买多个,而一件商品可以被多个用户购买,但这种关系是要体现在订单模块中,用户模型和商品并不产生之间的关系。
所以在本节中,我们需要确定的商铺模型与商品模型的关系。
废话不多说,让我们开始今天的开发!
- 商品需要标题,所以需要
标题
字段; - 商品需要有价格,所以需要
价格
字段; - 商品需要有上下架的状态,所以需要
上架状态
字段; - 商品是属于某个商铺的,需要
商铺ID
字段来标记; - 每个商品都应该由唯一的id,Rails已经为我们提供了默认的id字段,并且是主键,所以我们在这里不用自定义,直接使用默认即可;
- Rails还会自动帮我们维护两个字段:created_at, updated_at 。
字段 | 类型 | 长度 | 注释 | null | 默认值 |
---|---|---|---|---|---|
id | integer | 11 | 主键id,自动增长 | 否 | 0 |
title | string | 100 | 商品名称 | 否 | 空字符串 |
price | int | 11 | 商品价格:精确到分 | 否 | 空0串 |
published | unsigned tiny_int | 2 | 上架状态:0,未上架 1 已上架 | 否 | 1 |
shop_id | integer | 11 | 商铺id | 否 | 1 |
created_at | timestamp | --- | 创建时间 | 是 | 当前时间 |
updated_at | timestamp | --- | 修改时间 | 是 | 当前时间 |
- 添加和修改商品:title字段信息必须提供
- 添加和修改商品:price 字段信息大于等于0
- 添加和修改商品:published值只能为0或者1
$ git checkout -b chapter08
$ rails generate model product title:string price:integer published:integer shop_id:integer
Running via Spring preloader in process 2244
invoke active_record
create db/migrate/20210520075256_create_products.rb
create app/models/product.rb
invoke test_unit
create test/models/product_test.rb
create test/fixtures/products.yml
可以看到命令行帮我们生成了 db/migrate/20210520075256_create_products.rb
商品模型的迁移文件。
还帮我们生成了 app/models/product.rb
商品模型文件。
20210520075256_create_products.rb
class CreateProducts < ActiveRecord::Migration[6.1]
def change
create_table :products do |t|
t.string :title, null: false, default: ''
t.integer :price, null: false, default:0
t.integer :published, null:false, default:1
t.integer :shop_id, null:false, default:0
t.timestamps
end
add_index :products, [:shop_id, :title]
add_index :products, :title
end
end
我们这里创建了products表, 并添加了标题和商铺id的关联索引,这是因为同一家店铺内不允许商品同名,但是允许不同商铺的商品同名。
$ rails db:migrate
== 20210507090011 Createproducts: migrating ======================================
-- create_table(:products)
-> 0.0154s
== 20210507090011 Createproducts: migrated (0.0155s) =============================
执行结果显示创建了 products 表。
思路分析
我们需要做的验证:
- title:不能为空;
- title:同一家店铺的标题不允许重复;
- price:不能为空,整数,必须大于等于0。
app/models/product.rb
class Product < ApplicationRecord
validates :title, presence: true
validates :price, presence: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :published, inclusion: { in: [0, 1], message:"published can be only in [0 1]" }
validate :title_cannot_be_taken_in_self_shop
private
def title_cannot_be_taken_in_self_shop
if self.class.exists?(title: title, shop_id: shop_id)
errors.add(:title, "title cannot be taken in self shop")
end
end
end
我们利用了自定义方法title_cannot_be_taken_in_self_shop
来验证在同一家店铺中标题不重复。
$ rails console
2.7.2 :001 > p = Product.new({title:"", price:-1, published:2, shop_id:1})
=> #<Product id: nil, title: "", price: -1, published: 2, shop_id: 1, created_at: nil, updated_at: nil>
2.7.2 :002 > p.valid?
=> false
2.7.2 :003 > p.errors.messages
=> {:title=>["can't be blank"], :price=>["must be greater than or equal to 0"], :published=>["published can be only in [0 1]"]}
通过错误信息,可知3种验证都未通过
就像前面我们已经分析的:商铺和商品是一对多的关系,商品和店铺是一对一的关系。并且我们应该知道当商铺被删除,所属的商品也应该被删除。
# app/models/shop.rb
class Shop < ApplicationRecord
# ......
has_many :products, dependent: :destroy
end
end
# app/models/product.rb
class Product < ApplicationRecord
# ... ...
belongs_to :shop
# ... ...
end
我们使用Rails内置的 Minitest 测试框架来编写以及测试我们的应用。
对于单元测试的编写,我们需要进行两种基本类型场景测试:成功的场景和失败的场景。而模型的验证,在这里主要是针对模型中对字段的相关 validates 的针对性测试!
- 成功的场景
- 使用全部合法的参数(
合法的title,合法的price,合法的published, 合法的shop_id
)创建商品,断言:通过验证
- 使用全部合法的参数(
- 失败的场景
- 使用
非法的title,合法的price,合法的published,合法的shop_id
创建商品,断言:未通过验证 - 使用
重复的title和shop_id,合法的price,合法的published
创建商品,断言:未通过验证 - 使用
非法的published,合法的title,合法的price ,合法的shop_id
创建商品,断言:未通过验证 - 使用
非法的price ,合法法的published,合法的title ,合法的shop_id
创建商品,断言:未通过验证
- 使用
在使用Rails的命令创建模型product时,Rails还帮我们自动创建了测试文件已经与模型对应的测试用预定义数据文件:
test/fixtures/products.yml
我们可以修改这个文件的内容如下:
one:
title: 'first product'
price: 1
published: 1
shop: one
two:
title: 'second product'
price: 1
published: 1
shop: one
这里定义了两个商品:商品one
和 商品 two
, 然后我们就可以在测试文件中通过 products(:one)
来获取第一个商品的对象, products(:two)
来获取第二个商品的对象信息了。
测试的编写我们首先要确定文件路径:test/models/product_test.rb
, 如果你仔细观察,这个文件也是Rails帮我们自动创建的!现在开始编写相关测试吧!
-
使用全部合法的参数(
合法的title,合法的price,合法的published, 合法的shop_id
)创建商品,断言:通过验证test/models/product_test.rb
class productTest < ActiveSupport::TestCase setup do @p_one = products(:one) end # 使用合法参数 test 'valid: product with all valid things' do product = Product.new(title: "title123", price: 1, published: 1, shop_id: @p_one.shop.id) assert product.valid? end #... end
上面的测试首先定义
setup
方法创建一些公用数据。 -
使用
非法的title,合法的price,合法的published,合法的shop_id
创建商品,断言:未通过验证test/models/product_test.rb
test 'invalid: product with invalid title' do product = Product.new(title: "", price: 1, published: 1, shop_id: @p_one.shop.id) assert_not product.valid? end
-
使用
重复的title和shop_id,合法的price,合法的published
创建商品,断言:未通过验证test/models/product_test.rb
test 'invalid: product with taken title and shop_id' do product = Product.new(title: @p_one.title, price:1, published: 1, shop_id: @p_one.shop.id) assert_not product.valid? end
-
使用
非法的published,合法的title,合法的price ,合法的shop_id
创建商品,断言:未通过验证test/models/product_test.rb
test 'invalid: product with invalid published' do product = Product.new(title: 'first test', price: 1, published: 2, shop_id: @p_one.shop.id) assert_not product.valid? end
-
使用
非法的price ,合法的published,合法的title ,合法的shop_id
创建商品,断言:未通过验证test/models/product_test.rb
test 'invalid: product with invalid price' do product = Product.new(title: 'first test', price: -100, published: 1, shop_id: @p_one.shop.id) assert_not product.valid? end
$ rails test
... ...
Finished in 2.117990s, 17.4694 runs/s, 25.0237 assertions/s.
37 runs, 53 assertions, 0 failures, 0 errors, 0 skips
我们这里顺利通过测试。也许你在调试过程中会遇到许多小问题,慢慢解决,从中会学会很多东西。
关于上面的测试,我建议一个一个完成,不要都写完才测试!
$ git add .
$ git commit -m "set products model with validation"
我们完成了商品模型以及模型验证并完成了模型的相关测试,下节课我们将开发商品控制器相关功能!一定要跟上脚步,认真练习!!