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
}
代码也比较简单。首先通过path
和methon
字段来确定该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字段多了内容