GORM作为通用的全功能ORM,不仅对绝大数多关系型数据库提供了支持,同时也为主流的非关系型数据库进行了一定程度上的适配。除此以外,灵活、开发者友好、经过实践验证也是GORM的大特点之一。
本次,我们以简单电商为例,基于MySql、Kitex实现一个CRUD的后端服务。
安装
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
安装验证
先手动建立验证数据库gormdb。
create database gormdb;
验证表gormdb(同名),结构如下:
create table `gormdb`(
`id` int auto_increment,
`product_name` varchar(12) not null,
`product_price` int not null,
`product_number` int not null,
primary key(`id`)
)engine=InnoDB auto_increment=1 default charset=utf8;
然后编写go代码:
db, err := gorm.Open(
mysql.Open("test:test@tcp(127.0.0.1:3306)/gormdb?charset=utf8&parseTime=True&loc=Local"),
&gorm.Config{})
if err != nil {
panic("open DB failed")
}
fmt.Println("create table")
db.AutoMigrate(&Product{}) //注意,需要有对整个库的权限
fmt.Println("insert")
db.Create(&Product{ID: 1, Name: "p1", Price: 11})
var retP Product
fmt.Println("requiry-1")
db.First(&retP, 1)
fmt.Println(retP.Name)
fmt.Println("requiry-2")
db.First(&retP, "Product_name = ?", "p1")
fmt.Println(retP.ID)
fmt.Println("update")
db.Updates(map[string]interface{}{"id": 1, "Product_name": "np1"})
db.First(&retP, 1)
fmt.Println(retP.Name)
fmt.Println("delete")
db.Delete(&Product{}, "id = ?", 1) //默认权限是不包含删除的
效果如下
create table
insert
requiry-1
p1
requiry-2
1
update
p1
delete
CRUD编写
以简单电商为例。
需求分析
我们需要后端:
- 创建商品
- 提供商品信息(品名),价格,数量信息;
- 提供购买接口;
- 根据商品名、价格区间搜索商品;
具体地:
- 创建:
接收ID,商品名,价格,数量。其中ID不能重复,价格和数量不能为负。返回一个状态码表示是否成功即可。 - 全部查询: 因为是简单实现,不考虑分页等操作,返回全部商品信息和状态码即可。
- 查询信息:
接受品名或价格。返回商品和状态码列表。 - 购买:
接收ID。如果商品数大于0,返回成功;否则返回失败。
那么对应的Thrift接口文件则是:
namespace go api
enum ErrCode {
Success = 0,
ServiceErr = 101,
ParamErr = 102,
NotFoundErr = 104,
CreateErr = 201,
BuyErr = 301
}
struct Item {
1: string Name,
2: i64 ID,
3: i64 Price,
4: i64 Number
}
struct CreateRequest {
1: Item item
}
struct CreateResponse {
1: i64 Code
}
struct ResqResponse {
1: i64 Code
2: list<Item> Items
}
struct ProductReqByPrice {
1: i64 Price1,
2: i64 Price2
}
struct ProductReqByName {
1: string Name
}
struct BuyReq {
1: i64 ID
}
struct BuyResp {
1: i64 Code
}
service shop {
CreateResponse createProtuct(1: CreateRequest req),
ResqResponse requsetAll(),
ResqResponse requsetByName(1: ProductReqByName req),
ResqResponse requsetByPrice(1: ProductReqByPrice req),
BuyResp buy(1: BuyReq req)
}
构建
先通过Kitex生成代码:
func (s *ShopImpl) RequsetAll(ctx context.Context) (resp *api.ResqResponse, err error) {
return
}
func (s *ShopImpl) RequsetByName(ctx context.Context, req *api.ProductReqByName) (resp *api.ResqResponse, err error) {
return
}
func (s *ShopImpl) RequsetByPrice(ctx context.Context, req *api.ProductReqByPrice) (resp *api.ResqResponse, err error) {
return
}
func (s *ShopImpl) Buy(ctx context.Context, req *api.BuyReq) (resp *api.BuyResp, err error) {
return
}
填充测试
client侧,随便创建几个产品数据,然后调用RPC:
func main() {
client, err := shop.NewClient("shop", client.WithHostPorts("0.0.0.0:8888"))
if err != nil {
log.Fatal(err)
}
for i := 1; i < int(3); i += 1 {
newItem := api.Item{ID: int64(i), Name: ("p" + strconv.Itoa(i)), Price: int64(i*10 + i), Number: int64(i*100 + i)}
resp, err := client.CreateProtuct(context.Background(),
&api.CreateRequest{Item: &newItem})
if err != nil {
log.Fatal(err, resp.Code)
return
}
log.Println(resp.Code)
time.Sleep(time.Second)
}
resp, err := client.RequsetAll(context.Background())
if err != nil {
log.Fatal(err)
}
for _, i := range resp.Items {
fmt.Printf("%d %s %d %d\n", i.ID, i.Name, i.Price, i.Number)
}
}
效果:
2023/08/22 18:10:23 0
2023/08/22 18:10:24 0
1 p1 11 111
2 p2 22 222
编写dao
CRUD操作就全部规定到dao里。要有一个初始化类(顺便Model也放进去),还要有一个query类(部分代码省略)。 init.go:
func (Product) TableName() string {
return "Product"
}
var db *gorm.DB
func Init() {
var err error
db, err = gorm.Open(
mysql.Open("test:test@127.0.0.1:3306/gormdb?charset=utf8&parseTime=True&loc=Loacl"))
if err != nil {
panic(err)
}
err = db.AutoMigrate(&Product{})
if err != nil {
panic(err)
}
}
user.go:
func GetAllProduct(ctx context.Context) (items []*api.Item, err error) {
var retItems []Product
ret := db.Find(&retItems)
if ret.Error != nil {
err = ret.Error
}
items = make([]*api.Item, len(retItems))
for i, j := range retItems {
items[i] = &api.Item{}
items[i].ID = int64(j.ID)
items[i].Name = j.Name
items[i].Price = int64(j.Price)
items[i].Name = j.Name
}
return
}
func CreateNewProtuct(ctx context.Context, p *Product) (err error) {
ret := db.Create(&p)
if ret.Error != nil {
return ret.Error
}
return
}
func (p *Product) BeforeUpdate(tx *gorm.DB) (err error) {
if !tx.Statement.Changed("product_number") {
return errors.New("not changed. there are no such product or it already been seld out")
}
return
}
func BuyProduct(ctx context.Context, ID int64) (err error) {
db.Where("ID = ? and product_number > ?", ID, 0).Update("product_number", gorm.Expr("product_number - ?", 1))
return
}
这里为了确保购买操作成功,使用了BeforeUpdate钩子。因为是简单实践,没有写对应的异常处理等组件。
修改handler
这里将之前生成并填入测试数据的handler文件修改为调用dao的即可。
func GetAllProduct(ctx context.Context) (items []*api.Item, err error) {
var retItems []Product
ret := db.Find(&retItems)
if ret.Error != nil {
err = ret.Error
}
items = make([]*api.Item, len(retItems))
for i, j := range retItems {
items[i] = &api.Item{}
items[i].ID = int64(j.ID)
items[i].Name = j.Name
items[i].Price = int64(j.Price)
items[i].Name = j.Name
}
return
}
func CreateNewProtuct(ctx context.Context, p *Product) (err error) {
ret := db.Create(&p)
if ret.Error != nil {
return ret.Error
}
return
}
func (p *Product) BeforeUpdate(tx *gorm.DB) (err error) {
if !tx.Statement.Changed("product_number") {
return errors.New("not changed. there are no such product or it already been seld out")
}
return
}
func BuyProduct(ctx context.Context, ID int64) (err error) {
db.Model(&Product{}).Where("ID = ? and product_number > ?", ID, 0).Update("product_number", gorm.Expr("product_number - ?", 1))
return
}
再调用原先的测试client,可以发现数据库中多出了如下数据:
+----+--------------+---------------+----------------+
| id | product_name | product_price | product_number |
+----+--------------+---------------+----------------+
| 1 | p1 | 11 | 101 |
| 2 | p2 | 22 | 202 |
+----+--------------+---------------+----------------+
再尝试购买p2,如下:
| 2 | p2 | 22 | 201 |
可见,CURD功能实现成功。
总结
到此一个简单的电商服务后端就建立起来了(虽然CRUD估计有很多BUG)。gorm和Kitex分别提供了数据库读写和RPC服务,写起来还是挺省心的,但是不能忘了一些小细节,比如:数据层的初始化(在这里卡了好久);IDL的设计(中间还改了好多次);同时各种必要的组件还是要写的,这里主要展示CRUD的实践而没有着重处理。同时这份代码和文章是同时写就的,可能有一些代码上变化没有同步到文章上,下面我给出这个项目的Gihub地址,有兴趣的可以参考一下(也只能当参考了)。