实践文章-使用 GORM连接数据库| 青训营

77 阅读10分钟

使用 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)
    

屏幕截图 2023-08-07 165541.png

  • 约定
    • 使用名为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)

屏幕截图 2023-08-10 161455.png

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 * ? + ?", 2100))

屏幕截图 2023-08-10 165216.png

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{123})
// 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%") 

屏幕截图 2023-08-10 171834.png

  • 软删除,实际开发中使用的
    • 在结构体中额外定义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;
}

屏幕截图 2023-08-10 172653.png

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()

屏幕截图 2023-08-10 174621.png

提供了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

屏幕截图 2023-08-10 175711.png

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
}

屏幕截图 2023-08-10 180450.png

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")
}
7.2.10 gorm生态

屏幕截图 2023-08-10 181714.png