GORM | 青训营笔记

141 阅读7分钟

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

database/sql包

基本用法

先使用driver+DSN初始化数据库连接

// 打开数据库
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/hellp")

执行完一条sql,通过rows返回数据,处理完毕需要释放连接

// 查询
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
    xxx
}
// 主要是考虑到Scan过程中可能存在没有commit、rollback等产生异常,没有释放资源
defer rows.Close()

数据、错误处理:

var users []User
for rows.Next() {
    var user User
    err := rows.Scan(&user.ID, &user.Name)
    
    if err != nil {
        xxx
    }
    users = append(users, user)
}
if rows.Err() != nil {
    xxx
}

设计原理

image-20230117145512945

连接池的配置:

func (db *DB) SetConnMaxIdleTime(d time.Duration)
func (db *DB) SetConnMaxLifeTime(d time.Duration)
func (db *DB) SetMaxIdleConns(n int)
func (db *DB) SetMaxOpenConns(n int)

连接池状态:

func (db *DB) Stats() DBStats

操作过程的伪实现:

for i := 0; i < maxBadConnRetries; i ++ {
    // 从连接池获取连接或通过 driver 新建连接
    dc, err := db.conn(ctx, strategy)
    // 有空闲连接 -> reuse -> max life time
    // 新建连接 -> max open ...
    
    // 将连接放回连接池
    defer dc.db.putConn(dc, err, true)
    // validateConnection 有无错误
    // max lift time, max idle conns 检查
    
    // 连接实现drver.Queryer, driver.Execer 等interface
    if err == nil {
        err = dc.ci.Query(sql, args...)
    }
    is BadConn = errors.Is(err, driver.ErrBadConn)
    if !isBadConn {
        break;
    }
}

DB连接的几种类型:

  • 直接连接/conn
  • 预编译/Stmt
  • 事务/Tx

处理数据返回的几种方式

  • Exec/ExecContext -> Result:一般是想要返回操作的结果
  • Query/QueryContext -> Rows(columns):返回用rows封装的结果
  • QueryRow/QueryRowContext -> Rows(Rows 简化):同上

GORM基础使用

Gorm应该类似于java的mybatis,用driver

示例:

import(
    "gorm.io/gorm"
    "gorm.io/driver/mysql"
)
​
func main() {
    db, err := gorm.Open(
        mysql.Opem("user:password@tap(127.0.0.1:3306)/hello")
    )
    var users []User
    err = db.Select("id", "name").Find(&users, 1).Error
}

相比于直接用driver代码会简洁很多

CRUD的基本用法:

  • 操作数据库:

    db.AutoMigrate(&Product{})
    db.Migrator().CreateTable(&Product{})
    
  • 创建

    user := User{Name : "xiaoming", Age: 18, Birthday: time.Now()}
    result := db.Create(&user)
    
    user.ID			// 返回主键ID
    result.Error	// 返回error
    result.RowsAffected		// 返回受影响的行数
    
  • 批量创建

    var users = []User{{Name : "xiaoming1"}, {Name : "xiaoming2", {Name : "xiaoming3"}}
    db.Create(&users)
    db.CreateInBatches(users, 100)
                       
    for _, users := range users {
    	user.ID		// 123                       
    }
    
  • 读取

    var product Product
    db.First(&product, 1)						// 查询id为1的product
    db.First(&product, "code = ?", "L1212")		// 查询code为L1212的product
    
    result := db.Find(&users, []int{1, 2, 3})
    result.RowsAffected						// 返回找到的记录数
    errors.Is(result.Error, gorm.ErrRecordNotFound)		// First、Last、Take查不到数据
    
  • 更新某个字段

    db.Model(&product).Update("Price", 2000)
    db.Model(&product).UpdateColumn("Price", 2000)
    
  • 更新多个字段

    db.Model(&product).Updates(Product{Price : 2000, Code : "L1212"})
    db.Model(&product).Updates(map[string]interface{}{"Price" : 2000, "Code" : "L1212"})
    
  • 批量更新

    db.Model(&project).Where("price < ?", 2000).Updates(map[string]interface{}{"Price" : 2000})
    
  • 删除

    db.Delete(&project)
    

字段与数据库表的对应

  • 表名为struct name的 snake_cases 复数格式
  • 字段名为field name的snake_case 单数格式
  • ID/ id 字段为主键,如果为数字,则为自增主键
  • CreatedAt字段,创建时,保存当前时间
  • UpdatedAt字段,创建、更新时,保存当前时间
  • gorm.DeletedAt字段,默认开启soft delete模式

Gorm提供了关联:

image-20230117171432650

关联的操作:

image-20230117171454973

预加载、联表操作:

image-20230117171558203

级联删除:

image-20230117171730984

GORM设计原理

GORM所处的位置:

image-20230117172006908

SQL生成

例:

SELECT 'name', 'age', 'employee_number'
FROM 'users'
WHERE 
	role <> "manager" AND
	age > 35
ORDER BY age DESC
LIMIT 10 OFFSET 10
FOR UPDATE

用GORM调用:

db.Where("role <> ?", "manager").Where("age > ?", 35).Limit(100).Order("age desc").Find(&user)

除了最后一项为Finisher Method,决定GROM STATEMENT最终的类型,并执行,其余都为Chain Method分别添加其子句,Chain Method生成GORM Clauses。

image-20230117173751226

Where、limit都是将当前的参数翻译成GROM子句添加到GORM STATEMENT中去:

image-20230117174059083

Finisher Method:

image-20230117174157757

扩展子句:

image-20230117180500215

image-20230117181348996

目的是为了完成自由的扩展和定制

  • 多租户

    image-20230117181945804

  • 多数据库、读写分离

    image-20230117182149305

  • 加解密、混沌工程

插件扩展

插件是如何工作的:

image-20230117180854438

例如执行Create的过程就是调用一系列的Create Callback

image-20230117181223328

ConnPool

image-20230117182247127

Dialector

最佳实践

数据序列化与SQL表达式

SQL表达式更新创建

方法一:通过gorm.Expr使用SQL表达式

INSERT INTO "user_with_points" ("name", "location") VALUES ("xiaoming", ST_PointFromText("POINT(100 100)"))
db.Model(USer{}).Create(map[string]interface{}{
    "Name" : "xiaoming",
    "Location" : gorm.Expr("ST_PointFromText(?)", "POINT(100 100)"),
})
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))

方法二:使用GORMValue 使用SQL表达式 / SubQuery

type Location struct {
	X, Y int
}
func (loc Location) GormValue(ctx context.Contextm db *gorm.DB) clause.Expr {
	return gorm.Expr("ST_PointFromText(?)", fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y))
}
db.Create(User{Name : "xiaoming", Location : Location{x : 100, Y : 100}})
db.Model(&User{ID: 1}).Updates(User{Name : "xiaoming", Location : Location{X : 100, Y : 100}})

*方法三:通过 gorm.DB使用 SubQuery

 UPDATE "users" SET "company_name" = (SELECT name FROM companies WHERE company.id = users.company_id)
subQuery := db.Model(&Company{}).Select("name").Where("companies.id = users.compant_id")
db.Model(&user).Updates(map[string]interface{}{}{"company_name" : subQuery})

SQL表达式查询

方法一:使用gorm.Expr

SELECT * FROM 'users' WHERE 'location' = ST_PointFromText("POINT(100 100)")
db.Where("Location = ? ", gorm.Expr("ST_PointFromText(?)", "POINT(100 100)")).First(&user)

方法二:Struct定义 GormValuer

SQL语句同上

func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
	return gorm.Expr("ST_PointFromText(?)", fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y))
}
db.Where("location = ?", Location{X : 100, Y : 100}).First(&user)

方法三:自定义查询SQL实现接口 clause.Expression

type Expression interface {
	Build(builder Builder)
}
db.Find(&user, datetypes.JSONQuery("atteributes").HasKey("role"))
db.Clauses(Datatypes.JSONQuery("atteributes").HasKey("org", "name")).Find(&user)

方法四:SubQuery

db.Where("name in (?)", db.Model(&User{}).Select("name").Where("id > 10")).Find(&user)

数据序列化

type User struct {
	Name        []byte                 `gorm:"serializer:json"`
	Roles       Roles                  `gorm:"serializer:json"`
	Contracts   map[string]interface{} `gorm:"serializer:gob"`
	JobInfo     Job                    `gorm:"type:bytes;serializer:gob"`
	CreatedTime int64                  `gorm:"serializer:unixtime;type:time"`
	Password    Password
	Attributes  datatypes.JSON
}

自定义数据格式实现接口 Scanner,Valuer

func (j JSON) Value() (driver.Value, error) { /**/}
func (j *JSON) Scan(value interface{}) error{/**/}

自定义数据格式实现Serializer接口:

type Password string
type SerializerInterface interface{
	Scan(context.Context, f *schema.Field, dst reflect.Value, dbValue interface{}) error
	Value(context.Context, f *schema.Field, dst reflect.Value, fieldV interface{}) (interface{}, error)
}

批量数据操作

批量创建/查询

批量创建:

var users = []User{{Name : "xiaoming1"}, {Name : "xiaoming2"}, {Name : "xiaoming3"}}
db.Create(&users)
db.CreateInBatches(users, 100)
for _, user := range users {
	user.ID			// 123
}

批量查询:

rows, err := db.Model(&User{}).Where("role = ?", "admin").Rows()
for rows.Next() {
    // 方法一:sql.Rows Scan
	rows.Scan(&name, &age, &email)
    // 方法二:gorm ScanRows
	db.ScanRows(rows, &user)
}

DB.Where("role = ?", "admin").FindInBatches(&results, 100, func(tx *gorm.DB, vatch int) errpr{}) 

批量更新

忽略数据冲突

// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `id` = id;
db.Clauses(clause.OnConflict{DoNoting: true}).Create(&users)

// INSERT IGNORE INTO `users` ***; 
db.Clauses(clause.Insert{Modifier: "IGNORE"}).Create(&users)

数据冲突时,更新某些字段

// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `deleted_at` = NULL
db.Clauses(calse.OnConflict {
    Columns: 	[]clause.Column{{Name : "id"}},
    DoUpdates:	clause.Assignments(map[string]interface{}{"deleted_at" : nil}),
}).Create(&users)
// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `count` = GREATERST(count, VALUE(count));
db.Clauses(clause.OnConfilct{
    Columns: 	[]clause.Column{{Name : "id"}},
    DoUpdates:	clause.Assignments(map[string]interface{}{"count" : gorm.Expr("GREATEST(count, VALUES(count))")}),
}).Create(&users)

数据冲突时更新某些字段为新值:

// INSERT INTO `users` *** ON DUPLICATE KEY UPDATE `name` = VALUES(name), `age` = VALUIES(age);
db.Clauses(caluse.OnConflict {
   Columns: 	[]clause.Column{{Name : "id"}},
    DoUpdates:	clause.Assignments([]string{"name", "age"}),
}).Create(&users)

数据冲突时更新全部字段(除主键)为新值

db.Clause(clause.OnConflict{UpdateAll : true}).Create(&users)

批量数据加速操作

方法一:关闭默认事务

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
	SkipDefaulTransaction: true.
})
db.Create(&User)

tx := db.Session(&Session{SkipDefaultTransaction: true})
tx.Create(&user)

方法二:默认批量操作会调用Hooks方法,使用'SkipHooks'跳过

DB.Session(&gorm.Session{SkipHooks: true}).Create(&users)
DB.Session(&gorm.Session{SkipHooks: true}).CreateInBatches(users, 100)

方法三:使用Prepared statement

db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{PrepareStmt: true})
db.Create(&users)

混合使用:

tx := db.Session(&Session{
	PreparedStmt: true, SkipDefaultTransaction: true, SkipHooks: true, CreateBatchSize: 1000,
})
tx.Create(&users)

代码复用、分库分表、Sharding-分库分表

func Paginate(r *http.Request) func(db *gorm.DB)*gorm.DB {
    return func (db *gorm.DB) *gorm.DB {
		page, - := strconv.Atoi(r.Query("page"))
		if page -0 {
        	page = 1
    	}
        pageSize, _ := strconv.Atoi(r.Query("page_size"))
        switch {
        case pageSizee > 100 :
            pageSize = 100
        case pageSize <= 0:
            pageSize = 10
        }
        
        offset := (page - 1) * pageSize
        return db.Offset(offset).Limit(pageSize)
    }
}
// 代码共享
db.Scopes(PagInate(r)).Find(&users)
db.Scopes(Paginate(r)).Find(&articles)

分库分表:

传入数据分表:

func TableOfYear(user *Users, year int) func(db *gorm.DB) *gorm.DB{
    return func(db *gorm.DB) *gorm.DB {
        tableName := user.TableName() + strconv.Itoa(year)
        return db.Table(tableName)
    }
}
// SELECT * FROM users_2019
DB.Scopes(TableOfYear(user, 2019)).Find(&users)

使用传入数据分库(同一个连接)

func TableOfOrg(user *USer, dbName string) func(db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        tableName := dbName + "." + user.TableName()
        return db.Table(tableName)
    }
}
// SELECT * FROM org1.users
DB.scope(TableOfOrg(user, "org1")).Find(&users)

使用对象信息获取表名 /interface

func TableOfUser(user *User) func (db *gorm.DB) *gorm.DB {
    return func(db *gorm.DB) *gorm.DB {
        year := getYearInfoFromUserID(user.ID)
        return db.Table(user.TableName() + strconv.Itoa(year))
    }
}

Sharding-分库分表:

image-20230118103443532

混沌工程/压测

原本插入rick,10,模拟插入morty,100,检测系统能否发现这些错误

image-20230118105314480

压力测试:

image-20230118105538359

Logger/Trace

image-20230118105646702

Migrate-数据库迁移管理

image-20230118110233016

Gen代码生成

image-20230118110509722

image-20230118110900862

安全问题

用参数的方式传递都不会产生SQL注入的问题