gin-vue-admin学习(后端篇)—— 3.GORM

gorm是go语言的一个orm框架,用于与数据库交互,完成CRUD操作。gin-vue-admin项目,也是使用gorm框架来操作数据库的。在项目中,涉及到数据的增删查改的业务,内部实现原理大部分情况下都是后端与数据库交互所完成的。因此,可以从前端来找到有关数据的增删查改场景来学习gorm框架的使用。

1 查询操作

在前端 超级管理员 菜单下的子菜单项,均为和数据库操作相关的场景

在这里插入图片描述

我们可以用api管理来作为样例学习gorm框架。

在点击前端的 api管理 后,前端就会展示api的列表。因此可以查看前端是请求了后端哪个接口,以此来找到后端最终查询数据库的操作。
在这里插入图片描述

请求体如下,用于分页请求。

{"page":1,"pageSize":10}

注:虽然前端写的接口名字是/api/api/getApiList,但这api在前端经过了跨域处理,会将请求打到后端的/api/getApiList接口上,具体跨域如下图代码(前端代码根目录/vite.config.js):

在这里插入图片描述

因此,可以看出请求了后端的/api/getApiList接口。接下来就需要看这个接口实际做了什么操作,来获取到的api列表。

具体的后端代码如下

func (s *SystemApiApi) GetApiList(c *gin.Context) {
	var pageInfo systemReq.SearchApiParams
	_ = c.ShouldBindJSON(&pageInfo)
	if err := utils.Verify(pageInfo.PageInfo, utils.PageInfoVerify); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	if list, total, err := apiService.GetAPIInfoList(pageInfo.SysApi, pageInfo.PageInfo, pageInfo.OrderKey, pageInfo.Desc); err != nil {
		global.GVA_LOG.Error("获取失败!", zap.Error(err))
		response.FailWithMessage("获取失败", c)
	} else {
		response.OkWithDetailed(response.PageResult{
			List:     list,
			Total:    total,
			Page:     pageInfo.Page,
			PageSize: pageInfo.PageSize,
		}, "获取成功", c)
	}
}

调用apiService.GetAPIInfoList方法.在方法中最开始有一行代码是这样的:

db := global.GVA_DB.Model(&system.SysApi{})

这是将db绑定到system.SysApi所对应的表,该对象代码如下:

type SysApi struct {
	global.GVA_MODEL
	Path        string `json:"path" gorm:"comment:api路径"`             // api路径
	Description string `json:"description" gorm:"comment:api中文描述"`    // api中文描述
	ApiGroup    string `json:"apiGroup" gorm:"comment:api组"`          // api组
	Method      string `json:"method" gorm:"default:POST;comment:方法"` // 方法:创建POST(默认)|查看GET|更新PUT|删除DELETE
}

func (SysApi) TableName() string {
	return "sys_apis"
}

因此可以得知,上面global.GVA_DB.Model(&system.SysApi{})是将db绑定到sys_apis表上。具体可以点进去Gorm的源代码查看

// Model specify the model you would like to run db operations
//    // update all users's name to `hello`
//    db.Model(&User{}).Update("name", "hello")
//    // if user's primary key is non-blank, will use it as condition, then will only update the user's name to `hello`
//    db.Model(&user).Update("name", "hello")
func (db *DB) Model(value interface{}) (tx *DB) {
	tx = db.getInstance()
	tx.Statement.Model = value
	return
}

查看这个源代码,对于代码是什么意思,在我们当前使用阶段并不重要,重要的是要知道这段代码是要做什么操作,因此直接可以看注释。注释的大概意思就是使用了Modle后,后边你对db的操作都是对你绑定的这个表格的操作。

SysApi中除了自身的api相关参数外,还组合了global.GVA_MODEL结构体

type GVA_MODEL struct {
	ID        uint           `gorm:"primarykey"` // 主键ID
	CreatedAt time.Time      // 创建时间
	UpdatedAt time.Time      // 更新时间
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` // 删除时间
}

包含了主键ID,创建修改删除的时间字段。

其实最重要的就是上述的调用apiService.GetAPIInfoList方法.点进去后是有4个判断

	if api.Path != "" {
		db = db.Where("path LIKE ?", "%"+api.Path+"%")
	}

	if api.Description != "" {
		db = db.Where("description LIKE ?", "%"+api.Description+"%")
	}

	if api.Method != "" {
		db = db.Where("method = ?", api.Method)
	}

	if api.ApiGroup != "" {
		db = db.Where("api_group = ?", api.ApiGroup)
	}

从代码来看,主要是增加查找的条件,若Path不空,就相当于在SQL的where语句后增加path LIKE %xxxx%来指定path,下面的几个判断也是同理。

然后来了一句

err = db.Count(&total).Error

这个是判断增加上述条件后,目前结果是否有异常。接着往下看,在没有异常的时候,会执行下面的代码

db = db.Limit(limit).Offset(offset)
if order != "" {
  var OrderStr string
  // 设置有效排序key 防止sql注入
  // 感谢 Tom4t0 提交漏洞信息
  orderMap := make(map[string]bool, 5)
  orderMap["id"] = true
  orderMap["path"] = true
  orderMap["api_group"] = true
  orderMap["description"] = true
  orderMap["method"] = true
  if orderMap[order] {
    if desc {
      OrderStr = order + " desc"
    } else {
      OrderStr = order
    }
  } else { // didn't matched any order key in `orderMap`
    err = fmt.Errorf("非法的排序字段: %v", order)
    return apiList, total, err
  }

  err = db.Order(OrderStr).Find(&apiList).Error
} else {
  err = db.Order("api_group").Find(&apiList).Error
}

第1行的db.Limit(limit).Offset(offset)正好就是前端传入的分表字段。

由于go语言没有set类型,因此用string:bool来表示set类型,这从6-17行主要是判断用户是否需要按某个字段来排序。通过.Order(xxx)来指定使用哪个字段排序。

默认使用api_group字段来排序。因此,我们前端展示时候,默认是通过api组来排序的

err = db.Order("api_group").Find(&apiList).Error

在这里插入图片描述

然后Find(&apiList)将查询到的结果赋值给此结构体列表。

2 增加操作

对于GORM的增加操作,还是可以继续使用这个api管理的场景,前端上方有个新增的按钮,可以点击这个按钮,新增一个api。然后查看调用了哪个后端的接口。

如填入如下数据:

在这里插入图片描述
点确定后会调用对应的新增接口

在这里插入图片描述

可以看出,调用了/api/createApi接口,方法为post方法,请求体为:

{
  "path": "/test/api-test",
  "apiGroup": "test",
  "method": "POST",
  "description": "测试api"
}

接着就就可以去后端找到对应的接口来查看执行逻辑。

func (s *SystemApiApi) CreateApi(c *gin.Context) {
	var api system.SysApi
	_ = c.ShouldBindJSON(&api)
	if err := utils.Verify(api, utils.ApiVerify); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	if err := apiService.CreateApi(api); err != nil {
		global.GVA_LOG.Error("创建失败!", zap.Error(err))
		response.FailWithMessage("创建失败", c)
	} else {
		response.OkWithMessage("创建成功", c)
	}
}

首先将传入的请求体,反序列化为system.SysApi结构体对象。

对于此接口最主要的还是调用了apiService.CreateApi(api)来创建api。可以看下这个方法的代码

func (apiService *ApiService) CreateApi(api system.SysApi) (err error) {
	if !errors.Is(global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&system.SysApi{}).Error, gorm.ErrRecordNotFound) {
		return errors.New("存在相同api")
	}
	return global.GVA_DB.Create(&api).Error
}

代码也比较简单。首先通过pathmethon字段来确定该api是否被注册,若没有被注册,则注册该api

通过global.GVA_DB.Create(&api)就可以创建该api。

当写到这里时候,我突然有了个疑问,为什么在上面的查询操作里,使用了global.GVA_DB.Model(&system.SysApi{}),而在新增操作时候并没有使用。后来回过头再看了Model源代码的注释,其实使用Model的话,就类比我们写sql时候使用的where语句,将这些where语句与表对应。而我们新增操作时候,类比于写sql,并不需要where语句,所以就不需要Model方法。

3 修改操作

对于修改操作,从前端可以修改我们刚才新增的api

在这里插入图片描述

比如我把api的Post方法改为Get方法,点确定提交后,前端请求了后端的/api/updateApi方法,是POST方法,请求体内容如下

{
  "ID": 93,
  "CreatedAt": "2022-07-16T15:43:28.022+08:00",
  "UpdatedAt": "2022-07-16T15:43:28.022+08:00",
  "path": "/test/api-test",
  "description": "测试api",
  "apiGroup": "test",
  "method": "GET"
}

然后找到对应后端接口的方法

func (s *SystemApiApi) UpdateApi(c *gin.Context) {
	var api system.SysApi
	_ = c.ShouldBindJSON(&api)
	if err := utils.Verify(api, utils.ApiVerify); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	if err := apiService.UpdateApi(api); err != nil {
		global.GVA_LOG.Error("修改失败!", zap.Error(err))
		response.FailWithMessage("修改失败", c)
	} else {
		response.OkWithMessage("修改成功", c)
	}
}

最开始和createApi接口的方法一样,将传入的请求体反序列化为system.SysApi结构体对象。

最主要的还是调用了apiService.UpdateApi(api)此方法

func (apiService *ApiService) UpdateApi(api system.SysApi) (err error) {
	var oldA system.SysApi
	err = global.GVA_DB.Where("id = ?", api.ID).First(&oldA).Error
	if oldA.Path != api.Path || oldA.Method != api.Method {
		if !errors.Is(global.GVA_DB.Where("path = ? AND method = ?", api.Path, api.Method).First(&system.SysApi{}).Error, gorm.ErrRecordNotFound) {
			return errors.New("存在相同api路径")
		}
	}
	if err != nil {
		return err
	} else {
		err = CasbinServiceApp.UpdateCasbinApi(oldA.Path, api.Path, oldA.Method, api.Method)
		if err != nil {
			return err
		} else {
			err = global.GVA_DB.Save(&api).Error
		}
	}
	return err
}

首先判断是否修改了path和method字段,若修改了就查询数据库中是否已经有了该字段的api。后续先将api更新到casbin_rule表中,此表是之前写中间件学习记录时候,里边casbin用到的库,主要用于判断某个用户是否有权限使用某个api。

接下来是global.GVA_DB.Save(&api)操作,直接将api变量所对应的信息更新到数据库中,gorm官方文档是这样介绍Save的。其实就是根据主键来更新所有字段
在这里插入图片描述

4 删除操作

依旧可以用刚才创建的api来进行删除操作,在前端点击删除后,会去请求后端的/api/deleteApi方法,类型为POST,请求体为:

{
  "ID": 93,
  "CreatedAt": "2022-07-16T15:43:28.022+08:00",
  "UpdatedAt": "2022-07-16T16:03:01.181+08:00",
  "path": "/test/api-test",
  "description": "测试api",
  "apiGroup": "test",
  "method": "GET"
}

查看后端调用的代码

func (s *SystemApiApi) DeleteApi(c *gin.Context) {
	var api system.SysApi
	_ = c.ShouldBindJSON(&api)
	if err := utils.Verify(api.GVA_MODEL, utils.IdVerify); err != nil {
		response.FailWithMessage(err.Error(), c)
		return
	}
	if err := apiService.DeleteApi(api); err != nil {
		global.GVA_LOG.Error("删除失败!", zap.Error(err))
		response.FailWithMessage("删除失败", c)
	} else {
		response.OkWithMessage("删除成功", c)
	}
}

主要是调用了apiService.DeleteApi(api)方法

func (apiService *ApiService) DeleteApi(api system.SysApi) (err error) {
	var entity system.SysApi
	err = global.GVA_DB.Where("id = ?", api.ID).First(&entity).Error // 根据id查询api记录
	if errors.Is(err, gorm.ErrRecordNotFound) {                      // api记录不存在
		return err
	}
	err = global.GVA_DB.Delete(&entity).Error
	if err != nil {
		return err
	}
	CasbinServiceApp.ClearCasbin(1, entity.Path, entity.Method)
	return nil
}

这里首先通过主键来判断是否有该api,如果有的话,后边就调用global.GVA_DB.Delete(&entity)来删除这条记录。需要补充的是,这里的删除不是真正的删除,而是软删除。

在这里插入图片描述

此时我们查看数据库的内容,发现确实并没有删除,而是delete_at字段多了内容

在这里插入图片描述

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值