DATABASE/SQL与GORM设计实践 | 青训营笔记

409 阅读5分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。 PPT

目录

1. 理解 database/sql

database/sql 的基本用法,设计原理,基础概念介绍

2. GORM 使用简介

GORM的基本用法,Model定义,惯例约定,关联介绍

3. GORM 设计原理

SQL生成,扩展机制, ConnPool,Dialector等

4. GORM 最佳实践

GORM 最佳实践,企业级开发,FAQ等

理解 database/sql

1. 基本用法 - Quick Start

import driver实现,使用 driver + DSN 初始化 DB 连接。

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

数据、错误处理。(最后一个rows.Err()不要忘记!

package main

import (
   "database/sql"
   _ "github.com/go-sql-driver/mysql"
)

type User struct {
   ID   int64
   Name string
}

func main() {
   db, err := sql.Open("mysql", "user:password@(127.0.0.1:3306)/hello?parseTime=true")

   rows, err := db.Query("select id , name from users where id = ?", 1)
   if err != nil {
      // xxx
   }
   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
   }

}

2. 设计原理

1.png

2.png

操作过程伪实现。

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 life time , max idle conns 检查

   // 连接实现 driver.Queryer,driver.Execer 等 interface
   if err == nil {
      err = dc.ci.Query(sql, args...)
   }

   isBadConn = errors.Is(err, driver.ErrBadConn)
   if !isBadConn {
      break
   }
}

操作接口

DB连接的几种类型 :直接连接 / Conn , 预编译 / Stmt , 事务 / Tx 。

处理返回数据的几种方式 :Exec / ExecContext -> Result ,

Query / QueryContext -> Rows(Columns) ,

QueryRow / QueryRowContext -> Row(Rows简化) 。

GORM 使用简介

1. 背景知识

“设计简洁、功能强大、自由扩展的全功能 ORM”

设计原则 :API精简、测试优先、最小惊讶、灵活扩展、无依赖 可依赖

功能完善 :......

2. 基本用法

注意!!!!!!!!!!!!!结构体首字母一定要大写!!!!!!!!!!!

如下代码,使用 gorm 可达成最开始使用 datebase/sql 的代码一样的效果。

package main

import (
   "gorm.io/driver/mysql"
   "gorm.io/gorm"
)

type User struct {
   ID   int64
   Name string
}

func main() {
   // gorm

   db, err := gorm.Open(
      mysql.Open("root:929275@tcp(127.0.0.1:3306)/csj"))

   var users []User
   err = db.Select("id", "name").Find(&users, 1).Error
}

基本用法 - CRUD

// 操作数据库
db.AutoMigrate(&Product{})
db.Migrator().CreateTable(&Product{})

// 创建
user := User{Name: "Jinzhu"}
result := db.Create(&user)

user.ID             // 返回主键
result.Error        // 返回 error
result.RowsAffected // 返回影响的行数

// 批量创建
var users = []User{{Name: "xianghong"}, {Name: "xiaohuang"}, {Name: "xiaolan"}}
db.Create(&users)
db.CreateInBatches(users, 100)

for _, user := range users {
   user.ID
}

// 读取
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).UpdateColumns("Price", 2000)

// 更新多个字段
db.Model(&product).Updates(Product{Price: 2000, Code: "L1212"})
db.Model(&product).Updates(map[string]interface{}{"Price":2000, "Code":"L1212"})

// 批量更新
db.Model(&Product{}).Where("price < ?" , 2000).Updates(map[string]interface{}{"Price":2000, "Code":"L1212"})

//删除 - 删除 product
db.Delete(&product)

3. 模型定义

image.png

惯例约定

约定优于配置

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

一切都可配置

4. 关联介绍

见PPT

GORM 设计原理

image.png

本小节实在是听不太懂,还是功底不够,以后再来回顾。

1. SQL 是怎么生成的

先看一个简单的 SQL , 由一些基本子句构成,比如 SELECT 、WHERE等。有的里面包含很多的表达式构成,如 WHERE 。

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

再来看看 gorm 代码,由 Chain Method 和 Finisher Method 构成。

Chain Method : 添加相关子句的方法。如Where等

Finisher Method : 决定类型并且执行的方法。如Find方法,决定了是查找。

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

生成对应 SQL

SELECT * FROM users WHERE role <>"manager" AND age > 35 ORDER BY age desc LIMIT 100

GORM API 方法添加 Clauses 到 GORM Statement

// Where add conditions
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {
   tx = db.getInstance()
   if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {
      tx.Statement.AddClause(clause.Where{Exprs: conds})
   }
   return
}


// Limit specify the number of records to be retrieved
func (db *DB) Limit(limit int) (tx *DB) {
   tx = db.getInstance()
   tx.Statement.AddClause(clause.Limit{Limit: limit})
   return
}

// Find find records that match given conditions
func (db *DB) Find(dest interface{}, conds ...interface{}) (tx *DB) {
   tx = db.getInstance()
   if len(conds) > 0 {
      if exprs := tx.Statement.BuildCondition(conds[0], conds[1:]...); len(exprs) > 0 {
         tx.Statement.AddClause(clause.Where{Exprs: exprs})
      }
   }
   tx.Statement.Dest = dest
   return tx.callbacks.Query().Execute(tx)
}

自定义 Clause Builder ,方便扩展 Clause ,自由选择 Clauses 。

不同的数据库甚至不同版本的数据库支持的 SQL 不同,虽然都是用的 SQL 但是写 SQL 的时候甚至要写不同的版本。gorm 就希望把细节都隐藏起来,我们只要去实现一个版本就行。

对不同的数据库都支持,可以自己去扩展。

image.png

还可以扩展子句,使得用起来更加灵活。如下 :

db.Clauses(hints.New("MRR(idx1)")).Find(&User{})
// SELECT /*+ MRR(idx1) */ * FROM 'users'

db.Clauses(hints.UseIndex("idx_user_name")).Find(&User{})
// SELECT * FROM 'users' USE INDEX ('idx_user_name')

db.Clauses(hints.ForceIndex("idx_user_name", "idx_user_id").ForJoin()).Find(&User{})
// SELECT * FROM 'users' FORCE INDEX FOR JOIN ('idx_user_name','idx_user_id')

db.Clauses(hints.Comment("select", "master")).Find(&User{})
// SELECT /*master*/ * FROM 'users' ;
db.Clauses(hints.CommentBefore("insert", "node2")).Create(&user)
// /*node2*/ INSERT INTO 'users' ... ;
db.Clauses(hints.CommentAfter("where", "hint")).Find(&User{})

选择子句。不同数据库不一样,不改变代码的情况下兼容其他数据库。

/* PostgreSQL */
DELETE FROM users WHERE condition
DELETE FROM       WHERE
/* MySQL */
DELETE FROM users WHERE condition ORDER BY sge desc LIMIT 100
DELETE FROM       WHERE           ORDER BY          LIMIT
/* Clickhouse */
ALTER TABLE 'users' DELETE WHERE condition
ALTER/DELETE               WHERE

2. 插件是怎么工作的

image.png

自由定制扩展操作。

Callback 在每次操作时都会被全部调用。比如说注册一个 Create 的 Callback 。每次调用 Create 时会按照顺序调用所有的 Create 下的 Callback 函数。如下:

image.png

image.png

3. ConnPool 是什么

image.png

image.png

image.png

4. Dialector 是什么

定义各种数据库的连接,还可以定义 io 的缓存。

image.png

可以定制 SQL 生成 、 定制 GORM 插件 、 定制 ConnPool 、 定制企业特征逻辑。

GORM 最佳实践

本节看PPT吧,码量太大,顶不住

写不那么理解的代码实在是头疼

1. 数据序列化与 SQL 表达式

关于SQL的表达式,不同数据库之间的统一性也不是很好,各种 SQL 方法有很多不一致的地方。为了支持所有的情况 GORM 提供了一些表达式的支持。

SQL 表达式更新创建。

// 方法1:通过 gorm.Expr 使用 SQL 表达式
db.Model(User{}).Create(map[string]interface{}{
   "Name":     "jinzhu",
   "Location": gorm.Expr("ST_PointFromText(?)", "POINT(100 100)"),
})
//INSERT INTO "user_with_points" ("name", "location") VALUES ("jinzhu", ST_PointFromText("POINT(100 100)"));

// 方法2:使用 GORMValuer 使用 SQL 表达式 / SubQuery

type Location struct {
   X, Y int
}

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.Create(User{Name: "jinzhu", Location: Location{X: 100, Y: 100}})
db.Model(&User{ID:1}).Updates(User{Name: "jinzhu",Location: Location{X: 100,Y: 100}})

// 方法3:通过*gorm.DB 使用 SubQuery
subQuery := db.Model(&Company{}).Select("name").Where("companies.id = users.company_id")
db.Model(&user).Updates(map[string]interface{}{}{"company_name":subQuery})

2. 批量数据操作

3. 代码复用、分库分表、Sharding

4. 混沌工程 / 压测

5. Logger / Trace

6. Migrator

7. Gen 代码生成 / Raw SQL

8. 安全

end !