Gorm笔记 | 青训营笔记

276 阅读9分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第3天

今天上课学习了Gorm, Kitex, hertz三个框架, 今天先总结Gorm框架

Gorm简介

Gorm是一个orm框架, 主要用于减少访问数据层的代码量, 实现程序对象到关系数据库的映射关系. 与java中的MyBatis类似.

orm(Object-Relational Mapping), 对象关系映射

简单来说, orm框架让我们避免了冗余的sql语句, 转而以面向对象的方式取编写sql.

与其他orm框架, gorm也有两步必须的操作, 一是定义数据模型, 二是连接数据库

连接数据库

gorm原生支持的数据库共有四种, 分别是mysql, sqlserver, postgresql, sqllite四种

对于其他没有原生支持的数据库, gorm支持驱动复用, 即其数据库连接协议与以上四种相同时, 可以直接服用上述四种sql的连接驱动

gorm连接四种数据库, 主要区别时dsn(data source name)的不同.

以mysql为例, dsn各个部分分别表示为:

[username[:password]@] [protocol[(address)]]/dbname[?param1=value1&...paramN=valueN]

eg: dsn := "cjs:123456@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"

 dsn := "cjs:123456@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
 db,err := gorm.Open(mysql.Open(dsn),&gorm.Config{})

连接池

  • gorm连接数据库采用连接池的方式实现
  • 连接池中存放有限的活跃连接, 减少创建连接和关闭连接的时间
  • gorm连接池中的连接具有存活时间

gorm的连接池与java线程池的概念很接近, 都具有降低资源消耗, 提高响应速度等特点

除去线程池和连接池, 还有内存池以及实例池的概念与其较为接近.

简而言之, 连接池就是在里面存放一些个活跃的连接, 当需要时直接从连接池中取, 若没有可用的活跃连接便创建一个新的连接, 而若是当前池子已经满了, 那么就进入阻塞, 等待有连接可用.

这里的池子大小, 个人感觉和可同时连接服务器的最大数量是一个概念(存疑)

另外, 我们需要使用setMaxIdleConns(x int)手动设置活跃连接的最大数量, 一般默认为2.

并且, 我们需要使用setMaxOpenConns()设置打开数据库连接的最大数量, 默认值为0, 即表示可以无限连接数据库.

另外, 两个函数通常绑定在一起使用

因为gorm的每次sql操作都是从连接池中取出连接, 所以每次操作时连接不一定相同, 因此在事务等使用场景下, 需要固定某一连接, 即使用tx:=db.Begin()等操作固定连接, 在事务结束前不将其放入到连接中

共享会话连接

因为每次执行sql操作后, gorm返回一个初始化的*gorm.DB示例, 其不能安全地重复使用, 并且可能会被先前使用该实例的条件污染.

因此可以使用新建会话的方法, 创建一个可以共享的实例, 该是每次使用完都会初始化, 不必担心被先前条件污染

db:= DB.Where("name = ?","test").Session(&gorm.Session{})

数据模型

约定

与定义json等数据模型类似, gorm的数据模型也是通过struct来进行定义

 type User struct{
     ID uint
     Name string
 }

对于gorm而言, 约定大于配置

gorm使用ID作为主键, 将结构体的蛇形复数设定为表名, 将字段的蛇形设定为列名, 并使用UpdatedAt, CreatedAt等字段表示更新时间和创建时间(约定)

type UserLog struct{...} -> user_logs //表名, 蛇形复数

若是需要另外配置表名, 只需要为User实现TableName() string接口即可(应当遵循蛇形复数的约定)

默认值

gorm也是使用tag为字段定义默认值, 当插入时, 若是某字段插入值为0值, 则将会使用默认值替代0值

 type User struct {
   ID   int64
   Name string `gorm:"default:galeone"`
   Age  int64  `gorm:"default:18"`
 }

gorm.Model

gorm定义了一个gorm.Model的结构体, 可以直接将gorm.Model作为匿名结构体导入到自己定义的结构体中

 //gorm.Model
 type Model struct {
     ID uint                 `gorm:"primaryKey"`
     CreatedAt time.Time
     UpdatedAt time.Time
     DeletedAt gorm.DeletedAt `gorm:"index"`
 }

而对于CreatedAt, UpdatedAt字段, gorm会自动记录填充时间, 若需要更换时间戳方式/类型, 需要使用tag手动标注填充类型, 默认采用time.Time类型进行填充

而对于结构体嵌套, gorm会默认将匿名子结构体中的字段放入到父结构体中, 或是在非匿名结构体后打上tag标志将其拆分到父结构体中.

 type Author struct{
     Name string
     Email string
 }
 ​
 type Blog struct{
     Author Author `gorm:"embedded"`
 }
 //=======等价于=========
 type Blog struct{
     Author
 }
 //=====================
 type Blog struct{
     Author Author 'gorm:"embedded;embeddedPrefix_prefix"'
 }
 //=======等价于========
 type Blog struct{
     PrefixName string
     PrefixEmail string
 }

事务

Gorm默认在CUD等操作时开启事务, 主动关闭默认事务, 约会获得30%的性能提升

 //全局禁用默认事务
 db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
   SkipDefaultTransaction: true,
 })

通常情况下, 为了简便代码, 推荐使用db.Transaction() 来进行事务操作, 在db.Transaction中, 返回err时会自动回滚, 而返回nil时会自动提交事务.

 db.Transaction(func(tx *gorm.DB) error {
   // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
   if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
     // 返回任何错误都会回滚事务
     return err
   }
 ​
   if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
     return err
   }
 ​
   // 返回 nil 提交事务
   return nil
 })

Hook

Hook主要是在某些操作创建前后执行的函数

主要有BeforeSave,BeforeCreate,AfterSave,AfterCreate, BeforeUpdate,AfterUpdate, BeforeDelete,AfterDelete, AfterFind

若是在整个生命周期过程中出现任何错误, 将立即停止后续操作并回滚事务(也将会放弃修改)

若是需要跳过Hook方法, 可以开启SkipHooks会话模式

 DB.Session(&gorm.Session{SkipHooks: true}).Create(&user)

CRUD

Create

 //根据数据模型将数据插入到表中
 user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
 //插入成功后, 将表中, 改行对应的信息重新写回到user中, 主要是为了重写某些隐藏的字段
 result := db.Create(&user) // 通过数据的指针来创建
 //result.Error记录error信息, result.RowsAffected返回插入的记录数量

db.Create()中的参数可以是struct结构体, 也可以是结构体数组(用以批量插入), 也能通过map[string]interface{}{}进行插入

值得注意的是, 在使用map进行插入时, gorm会忽略掉需要自动填充的字段, 并且关联association也不会被触发

gorm也允许使用SQL语句进行插入, 通常用clause.Expr{SQL:"这里写sql语句",Vars:[]interface{}}替换map中对应的值

关联创建

关联先可以简单视为数据模型中的非匿名嵌套结构体

在创建关联数据时, 如果关联数据值非0, 这些字段会被upsert, 且相应的Hook方法也会触发

可以通过Omit等跳过关联数据的保存

 db.Omit(clause.Associations).Create(&uset)//跳过所有关联

Upsert

Upsertupdateinsert的结合体, 执行时二者取其一, 优先取update

Read

查找单个对象

 db.First(&user)//按照主键升序, 获取第一条记录, 不推荐使用, 因为会返回err
 db.Find(&user,id)//推荐使用, 因为不回返回err, 并且可以接收slice, 查找时也可以指明id
 db.First(&user, 10)
 // SELECT * FROM users WHERE id = 10;
 ​
 db.First(&user, "10")
 // SELECT * FROM users WHERE id = 10;
 ​
 db.Find(&users, []int{1,2,3})
 // SELECT * FROM users WHERE id IN (1,2,3);

查找全部对象

 db.Find(&([]User{}))
 //SELECT * FROM users;

条件查询

gorm可以db后任意叠加where()来添加查询条件, 对于where语句中出现的数值, 可以使用?充当占位符, 并在后方填写对应value(与printf()类似)

 // Get first matched record
 db.Where("name = ?", "jinzhu").First(&user)
 // SELECT * FROM users WHERE name = 'jinzhu' ORDER BY id LIMIT 1;
 ​
 // Get all matched records
 db.Where("name <> ?", "jinzhu").Find(&users)
 // SELECT * FROM users WHERE name <> 'jinzhu';
 ​
 // IN
 db.Where("name IN ?", []string{"jinzhu", "jinzhu 2"}).Find(&users)
 // SELECT * FROM users WHERE name IN ('jinzhu','jinzhu 2');
 ​
 // LIKE
 db.Where("name LIKE ?", "%jin%").Find(&users)
 // SELECT * FROM users WHERE name LIKE '%jin%';
 ​
 // AND
 db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
 // SELECT * FROM users WHERE name = 'jinzhu' AND age >= 22;
 ​
 // Time
 db.Where("updated_at > ?", lastWeek).Find(&users)
 // SELECT * FROM users WHERE updated_at > '2000-01-01 00:00:00';
 ​
 // BETWEEN
 db.Where("created_at BETWEEN ? AND ?", lastWeek, today).Find(&users)
 // SELECT * FROM users WHERE created_at BETWEEN '2000-01-01 00:00:00' AND '2000-01-08 00:00:00';

同时, gorm也可以使用struct和map进行条件查找.

 // Struct
 db.Where(&User{Name: "jinzhu", Age: 20}).First(&user)
 // SELECT * FROM users WHERE name = "jinzhu" AND age = 20 ORDER BY id LIMIT 1;
 ​
 // Map
 db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)
 // SELECT * FROM users WHERE name = "jinzhu" AND age = 20;
 ​
 // Slice of primary keys
 db.Where([]int64{20, 21, 22}).Find(&users)
 // SELECT * FROM users WHERE id IN (20, 21, 22);

排序

 db.Order("age desc, name").Find(&users)
 // SELECT * FROM users ORDER BY age desc, name;
 ​
 // Multiple orders
 db.Order("age desc").Order("name").Find(&users)
 // SELECT * FROM users ORDER BY age desc, name;
 ​
 db.Clauses(clause.OrderBy{
   Expression: clause.Expr{SQL: "FIELD(id,?)", Vars: []interface{}{[]int{1, 2, 3}}, WithoutParentheses: true},
 }).Find(&User{})
 // SELECT * FROM users ORDER BY FIELD(id,1,2,3)

Update

Save会保存所有字段, 包括零值字段.

 db.First(&user)
 user.Name = "jinzhu 2"
 user.Age = 100
 db.Save(&user)
 // UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;

而使用struct{}更新时, 只会更新非零值, 若是需要保存零值, 请使用map或是'save'进行保存

Delete

删除单条记录时, 需要指明主键, 否则会删除满足条件的所有记录

那么当delete执行时不含主键, 便会删除所有匹配的记录

但是gorm执行批量删除时, 必须加一些额外的条件, 否则会返回ErrMissingWhereClause错误

 db.Delete(&User{}).Error // gorm.ErrMissingWhereClause
 ​
 db.Where("1 = 1").Delete(&User{})
 // DELETE FROM `users` WHERE 1=1
 ​
 db.Exec("DELETE FROM users")
 // DELETE FROM users
 ​
 db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})
 // DELETE FROM users

与read中相似, delete指定主键时, 主键可以是数字, 字符串或者是整型数组

软删除

当数据结构中包含DeletedAt字段时, 将默认开启软删除, 此时再执行delete语句, 不回将其真正删除, 而是会为DeletedAt填充上当前时间

并且软删除之后, 无法通过寻常的查询方法获取到该记录

只能使用Unscoped寻找被软删除的数据

 //查找软删除的数据
 db.Unscoped().Where("age = 20").Find(&users)
 // SELECT * FROM users WHERE age = 20;

那么, 再开启软删除后, 也只能通过Unscoped对数据进行永久删除

 //永久删除
 db.Unscoped().Delete(&order)
 // DELETE FROM orders WHERE id=10;

参考链接

  1. GORM指南
  2. ORM框架简介
  3. gorm 连接池_Go组件学习——手写连接池并没有那么简单
  4. gorm 连接池使用
  5. GORM快速入门教程