使用 GORM连接数据库
[TOC]
7.1 简单介绍三件套
三件套介绍
- Gorm:已经迭代了10年+的功能强大的ORM框架,在字节内部被广泛使用且拥有非常丰富的开源扩展。
- Kitex:字节内部的Golang微服务RPC框架,具有高性能、强可扩展性的主要特点,支持多协议并且拥有丰富的开源扩展。
- Hertz:字节内部的HTTP框架,参考了其他开源框架的优势,结合字节跳动内部的需求,具有高易用性、高性能、高扩展性特点。
7.2 GORM
DB操作
注意:
- 链式调用时,where API 都是拼SQL的,Find、Update、Delete等是真正执行SQL;在这之后的条件都不生效。
- 链式调用,需要得到返回的对象,由此可以获取err等
7.2.1 gorm的基本使用
-
定义gorm model:对应数据库的一张表,字段就是表中的每一个字段
-
为model定义表名:gorm提供
TableName()这个接口,需要返回字符串,这是一个约定 -
连接数据库:初始化数据库的链接
gorm.Open()mysql.Open()gorm.Config{}传递一些自定义的配置db,err :=gorm.Open( mysql.Open("user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{} ) -
创建数据:
db.Create()支持创建一条数据和多条数据。创建一条数据时,传递的是一个对象
创建多条数据时,传递的时一个切片(数组)
db.Create(&Product{Code:"D42,Price:100}) -
查询数据
先声明一个结构体,传递结构体的指针
db.First()仅支持查一条数据。也有支持查询多条的方法,后面介绍var product Product //先声明 db.First(&product,1) //传指针,因为需要反写到结构体里面 //传递的条件是整型,那么根据整型主键查找 db.First(&prodyct,"code = ?","D42") //传递基础的查询条件;查询code字段值为D42的记录 -
更新数据
.Model(&结构体)用来传递表名,或.Table(字符串)db.Model(&).Update(字段名,更新的值)更新单列数据db.Model(&).Updates()更新多个字段,支持传递一个结构体/Map结构体:有一个默认值,如int默认为0,字符串默认为空,gorm只更新非零值字段
Map:可用来更新零值
db.Model(&product).Update("Price",200)//更新一列数据 db.Model(&product).Updates(Product{Price:200,Code:"F42"}) //使用结构体只更新非零值字段 db.Model(&product).Updates(map[string]interface{}{Price:200,Code:"F42"})//可更新零值 -
删除数据
db.Delete(&结构体,条件)条件为整型,按主键删除db.Delete(&product,1)
- 约定
- 使用名为ID的字段作为主键,默认
- 没有使用TableName定义表名时,Gorm使用结构体的蛇形负数作为表名
- 字段名的蛇形作为列名
- 使用CreateAt、UpdateAt字段作为创建、更新时间
- 蛇形(Snake case)是一种命名风格,通常用于变量、函数和结构体的命名。蛇形命名法使用小写字母 和下划线(_) 来分隔单词。
#####7.2.2 gorm支持的数据库
目前支持MySQL、SQL server、postgreSQL、SQLite
gorm通过驱动来连接数据库,如果需要连接其他类型的数据库,可以复用/自行开发驱动。
MySQL的DSN:github.com/go-sql-driv…
//连接SQLServer数据库为例
import(
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
)
// https://github.com/denisenkom/go-mssqldb
dsn := "sqlserver://gorm:LoremIpsum86@localhost:9930?database=gorm"
db, err := gorm.Open(sqlserver.Open(dsn),&gorm.Config{})
7.2.3 gorm创建数据
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type Product struct {
ID uint `gorm:"primarykey"`
Code string `gorm:"column: code"` //定义一个列名
Price uint `gorm:"column: user_id"` //可以使列名和结构体字段名不一致
}
func main(){
//连接数据库
db,err := gorm.0pen(mysql.0pen(dsn: "username:password@tcp(localhost:9910)/database?charset=utf8")&gorm,Config{})
if err != nil :“failed to connect database"*
// 创建一条数据
p := &Product{Code:"D42"} //没有传主键,可能会有自增主键这样的设置
res := db.Create(p) //是链式调用,返回的对象是gorm,去获取err
fmt.Println(res.Error) // 获取 err
fmt.Println(p.ID)// 返回插入数据的主键
// 创建多条
products := []*Product{{Code:"D41"},{Code:"D42"},{Code:"D43"}}
res = db.Create(products)
fmt.Println(res.Error) // 链式调用返回的对象获取 err
for _, p := range products {
fmt.Printin(p.ID)
}
}
唯一索引冲突,如何处理?
使用Upsert,使用clause.OnConflict处理数据冲突
// 以不处理冲突为例,创建一条数据
p := &Product{Code:"D42",ID: 1}
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&p)//只把数据插进去
如何使用默认值?
通过使用gorm提供的标签default
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
7.2.4 gorm查询数据
gorm提供的API:First()、Find()
如何与条件查询去联动
-
First()默认查一条数据,拿第一条数据,按主键升序;查询不到返回ErrRecordNotFound
-
Find()查询一组数据,查询不到的话返回空数组,不会返回错误
-
db.Where().Find()因为是链式调用,因此若想得到error和条数的话,需要把返回的对象保存起来
-
In查询,可以传递数组
-
LIKE查询 -
复杂
AND查询,可以拆成两个where操作,gorm会帮助我们进行拼接 -
Where()可以支持传递 结构体 和 map ,少用但也有 -
使用结构体:gorm只会查询非零值条件。意味着字段值为0、false或其他零值,该字段不会被用于构建查询条件,更新类似。有零值需求时可以使用map
-
日常更多使用.Where()+Find()、Update()、Delete()
db,err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8"),&gorm.Config{})
if err != nil {
panic( "failed to connect database")
}
//获取第一条记录(主键升序),查询不到数据则返回 ErrRecordNotFound
u := & User{}
db.First(u) //SELECT * FROM users ORDER BY id LIMIT 1:
//查询多条数据
users := make([]*User,0)
//链式调用,需要保存返回的调用
result := db.Where( "age > 10").Find(&users) // SELECT * FROM users where age > 10;
fmt.Println(result.RowsAffected)// 返回找到的记录数,相当于 Len(users)
fmt.PrintIn(result.Error) //returns error
// IN SELECT * FROM users WHERE name IN ('jinzhu',jinzhu 2');
db.Where("name IN ?",[]string("jinzhu","jinzhu 2"}).Find(&users)
//LIKE SELECT * FROM users WHERE name LIKE '%jin%';
db.Where( "name LIKE ?","%jin%").Find(&users)
//AND SELECT*FROM users WHERE name =jinzhu'ANDage >= 22;
//可以拆成两个where操作,gorm会帮助我们进行拼接
db.Where("name = ? AND age >= ?" ,"jinzhu","22").Find(&users)
// SELECT*FROM users WHERE name ="jinzhu";
db.Where(&User{Name:"jinzhu", Age: 0}).Find(&users)
// SELECT * FROM users WHERE name ="jnzhu" AND age = 0;
db.Where(map[string]interface{}{"Name": "jinzhu""Age": 0}).Find(&users)
7.2.5 gorm更新数据
.Model()设置一个表名。User结构体如果实现了TableName这个接口就是用该接口所提供的表名;如果没实现的话就选择蛇影负数
.Where()设置查询条件
- 更新单个列,
Update(),没有位置去传递表名,因此需要.Model去设置表名,否则的话会报“没有表名”的错误 - 更新多个列,
Updates()- 使用结构体,没有where查询条件的话根据主键ID的值去做更新
- 使用map,用以规避零值更新问题
- 更新选定字段,gorm提供的 select API
Select传递一个字符串,选定要更新的字段
- 使用gorm做表达式更新,常见
- 注意使用Struct更新时,指挥更新非零值,若需要更新零值,使用map更新或Select选择字段
//条件更新单个列
// UPDATE users SET name='hello',updated_at='2013-11-17 21:34:10' WHERE age > 18;
db.Model(&User{ID: 111}).Where("age > ?",18).Update("name","hello")
//更新多个列
//根据struct更新属性,只会更新非零值的字段
// UPDATE users SET name='hello',age=18,updated_at = '2013-11-17 21:34:10' WHERE d = 111;
//没有where查询条件,根据主键ID的值去做更新
db.Model(&User{ID: 111}).Updates(User{Name: "hello",Age: 18})
// 根据 map更新属性
// UPDATE users SET name='hello,age=18,actived=false, updated_at='2013-11-17 21:34:10' WHERE id=111;
db.Model(&User{ID: 111}).Updates(map[string] interface{}{"name": "hello", "age": 18, "actived": false})
//更新选定字段
//UPDATE users SET name='hello' WHERE id=111;
db.Model(&User{ID: 111}).Select( "name").Updates(map[string] intenface{}{"name": "hello","age": 18,"actived": false})
// SQL 表达式更新
// UPDATE "products" SET "price" = price * 2 + 100,"updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
db.Model(&User{ID: 111}).Update("age", gorm.Expr( "age * ? + ?", 2, 100))
7.2.6 gorm删除数据
- 物理删除,删了就真删了
- 传递整型
- 传递字符串,自动识别成主键
// DELETE FROM users WHERE id = 10;
db.Delete(&User{},10)
// DELETE FROM users WHERE id = 10;
db.Delete(&User{},"10")
// DELETE FROM users WHERE id IN (1,2,3)
db.Delete(&User{},[]int{1,2,3})
// DELETE from users where name LIKE "%jinzhu%";
db.Where( "name LIKE ?", "%jinzhu%").Delete(User{})
// DELETE from users where name LIKE "%jinzhu%".
db.Delete(User{},"email LIKE ?","%jinzhu%")
- 软删除,实际开发中使用的
- 在结构体中额外定义
Deleted字段,需要使用gorm.DeletedAt。如此结构体被赋予了软删的能力。 - 若实现flag删除或其他,文档里有gorm.cn/zh_CN/docs/…
- 单个删除
- 批量删除,可以传递where条件
- 查询时不需要做额外操作就会忽略被软删的记录
- 不希望忽略:
Unscoped()
- 在结构体中额外定义
package main
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
type User struct {
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
Deleted gorm.DeletedAt
}
func main() {
db,err := gorm,Open(mysql.Open( "username:password@tcp(localhost:9910)/database?charset=utf8"),&gorm.Config{})
if err != nil {
panic("failed to connect database")
}
// 删除一条
u := User{ID:111} // user 的ID是111
db.Delete(&u)// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE d =111;
// 批量删除
db.Where("age = ?",20).Delete(&User{})// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE aqe = 20
users := make([]*User,0)
//在查询时会忽略被软删除的记录
db.Where("age = 20").Find(&users) // SELECT * FROM uers WHERE aqe = 20 AND deleted-at IS NULL;
//在查询时不会忽略被软删除的记录
db.Unscoped().Where("age = 20").Find(&users) // SELECT * FROM users WHERE age = 20;
}
7.2.7 gorm事务
保证数据的一致性
提供了Begin、Commit、Rollback方法
- 开启事务
db.Begin(),返回gorm对象;开始后使用tx而不是db - 注意:存在忘写rollback和commit导致数据库的链接泄露——使用transaction
db,err := gorm.Open(mysql.Open("username:password@tcp(localhost:9910)/database?charset=utf8")&gorm.Config{})
if err != nil {
panic( "failed to connect database")
}
tx := db.Begin() // 开始事务
//在事务中执行一些 db 操作 (从这里开始,您应该使用‘tx’而不是‘db’)
if err = tx.Create(&User{Name: "name"}).Error; err != nil {
tx.Rollback()//遇到错误时回滚事务
return
}
if err = tx.Create(&User{Name: "name1"}).Error; err != nil {
tx.Rollback()
return
}
// 提交事务
tx.Commit()
提供了Transaction方法用于自动提交事务,避免漏写Commit、Rollback
db.Transaction(),传递gorm.DB的对象- 不需要rollback了
- 当panic或返回err时,会自动调用rollback
- 返回nil,自动提交事务
- 实际:使用defer进行panic和return的拦截
db,err ;= gorm,Open(mysgl.Open("username:password@tcp(localhost:9910)/database?charset=utf8")&gorm.Config{})
if err != nil {
panic("failed to connect database")
if err = db.Transaction(func(tx *gorm.DB) error {
if err = tx.Create(&User{Name: "name"}).Error; err != nil{
return err
}
if err = tx.Create(&User{Name: "namel"}).Error; err != nil{
tx.Rollback()//不需要了
return err
}
return nil
}); err != nil {
return
7.2.8 gorm 的 Hook
使用场景:创建前创建后,查询前查询后需要进行一些操作
- before:可以进行参数校验
- after:
- 任何hook返回错误,gorm将停止后续的操作并返回滚事务。
- 读操作时没有事务,因为不需要。
问题:会不会有不一致的问题——使用hook时,gorm默认开启默认事务
创建/删除/更新都带默认事务,是默认开启的,因此会有一些性能问题。
type User struct{
ID int64
Name string `gorm:"default:galeone"`
Age int64 `gorm:"default:18"`
}
type Email struct{
ID int64
Name string
Email string
}
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
if u.Age <0{
return errors.New("can't save invalid data")
}
return
func (u *User) AfterCreate(tx *gorm.DB) (err error) {
return tx.Create(&Email{ID: u.ID,Email: u.Name + "@***.com"}).Error
}
7.2.9 gorm 性能提高
①关闭默认事务
对于写操作(创建、更新、删除),为了确保数据的完整性,GORM 会将它们封装在事务内运行但这会降低性能,你可以使用 skipDefaultTransaction 关闭默认事务。
②使用预编译语句
使用 PrepareStmt 缓存预编译语句可以提高后续调用的速度,本机测试提高大约 35 %左右。
db, err := gorm.Open (mysal.Open("username:password@tcp(localhost:9910)/database?charset=utf8"&gorm.Config{
SkipDefaultTransaction: true,//关闭默认事务
PrepareStmt: true},// 缓存预编译语句
)
if err != nil {
panic( "failed to connect database")
}