这是我参与「第五届青训营 」伴学笔记创作活动的第 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
}
设计原理
连接池的配置:
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 // 1、2、3 } -
读取
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提供了关联:
关联的操作:
预加载、联表操作:
级联删除:
GORM设计原理
GORM所处的位置:
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。
Where、limit都是将当前的参数翻译成GROM子句添加到GORM STATEMENT中去:
Finisher Method:
扩展子句:
目的是为了完成自由的扩展和定制
-
多租户
-
多数据库、读写分离
-
加解密、混沌工程
插件扩展
插件是如何工作的:
例如执行Create的过程就是调用一系列的Create Callback
ConnPool
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 // 1、2、3
}
批量查询:
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-分库分表:
混沌工程/压测
原本插入rick,10,模拟插入morty,100,检测系统能否发现这些错误
压力测试:
Logger/Trace
Migrate-数据库迁移管理
Gen代码生成
安全问题
用参数的方式传递都不会产生SQL注入的问题