GORM于MySql上的CRUD实践 | 青训营

35 阅读4分钟

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地址,有兴趣的可以参考一下(也只能当参考了)。

github.com/2B-dada/smi…