现状
现有orm
依赖库使用beego
自带orm
包,存在以下问题(gorm也有类似问题)
- 日志输出格式不受框架控制,不是
json
格式,不带trace_id
,没有机器实例信息等等 Raw sql
和orm
混用,风格不统一(复杂查询orm
搞不定)- 没有事务相关封装
- 没有统一封装
log/tracing/metrics
等信息,需要手动输出日志 - 没有超时控制
现有服务中,orm没法根据trace_id打印日志,导致定位问题困难.
sqlx
是标准库sql
的升级版
在sqlx的基础上线封装一套新的api以适应业务,实现以下目标
- 支持多db实例
- 记录 sql 执行日志/cost
- 上报 opentracing 追踪数据
- 汇总 prometheus 监控指标
- 尽可能使api简洁,越少越好,标准库的api实际只有5个(连事务)
sqlx存在缺点
- 不够orm,只有部分操作可以真正的实现orm,例如insert,update(以id为查询条件时)
- 写法跟orm不同,需要适应一下
- api略多,5个基础操作+1个复杂exec+1个复杂查询+1个事务
注意: api设计的时候没考虑数据库字段为NULL的情况,建表的时候也不要允许字段为NULL
使用示例
假设数据库中的表映射到model如下
type User struct {
ID int
Name string
Age int
CreateTime time.Time
}
// TableName 返回表面,必须实现
func (u User) TableName() string {
return "t_user"
}
// KeyName 返回主键,必须实现
func (u User) KeyName() string {
return "id"
}
-
单行查询
// 选择某个数据库,可以支持多实例
conn := sqlx.Get(ctx, "db1")
ctx := context.TODO()
var u User
err = conn.GetContext(ctx, &u, "select * from users where id = ?", id)
if err != nil {
return
}
-
多行查询
// 选择某个数据库,可以支持多实例
conn := sqlx.Get(ctx, "db2")
ctx := context.TODO()
var users []User
err = conn.SelectContext(ctx, &users, "select * from users order by id desc")
if err != nil {
return
}
-
Insert
// 选择某个数据库,可以支持多实例
conn := sqlx.Get(ctx, "db3")
ctx := context.TODO()
u := User{
Name: "demo",
Age: 18,
}
result, err := conn.InsertContext(ctx, u)
if err != nil {
return
}
id, _ := result.LastInsertId()
-
Update
// 选择某个数据库,可以支持多实例
conn := sqlx.Get(ctx, "db1")
ctx := context.TODO()
u.Name = "bar"
u.ID = int(id)
_, err = conn.UpdateContext(ctx, u)
if err != nil {
return
}
-
Delete
// 选择某个数据库,可以支持多实例
conn := sqlx.Get(ctx, "db2")
ctx := context.TODO()
u.ID = int(id)
_, err = conn.DeleteContext(ctx, u)
if err != nil {
return
}
-
复杂查询
以上是标准的单表查询,对于复杂的查询,例如连表操作,需要使用类似原生的api
// 选择某个数据库,可以支持多实例
conn := sqlx.Get(ctx, "db2")
ctx := context.TODO()
rows, err := conn.QueryContext(ctx, "select a.t, a.B, b.T, b.N from t_test as a left join t_demo as b on a.id = b.xx_id")
if err != nil {
return
}
// ........
-
事务
func dao_func(ctx context.Context) {
conn := Get(ctx, "")
tx, err := conn.Beginx()
if err != nil {
panic(err)
}
defer func() {
if p := recover(); p != nil {
// 回滚,继续向上panic
tx.Rollback()
panic(p)
} else if err != nil {
// 回滚,向上抛 err
tx.Rollback()
} else {
// 提交事务
err = tx.Commit()
}
}()
// 事务1
u := user{ID: 11, Name: "lalala", Age: 100}
if _, err = tx.UpdateContext(ctx, u); err != nil {
return
}
// 事务2
if err = trans(ctx, tx); err != nil {
return
}
// 事务n...
return
}
// trans 事务中的其他操作
func trans(ctx context.Context, conn *Tx) (err error) {
u := user{ID: 1000, Name: "None", Age: 999}
result, err := conn.UpdateContext(ctx, u)
if err != nil {
return
}
affect, err := result.RowsAffected()
if err != nil {
return err
}
if affect < 1 {
err = fmt.Errorf("no affect")
return
}
return
}
拦截器
另外有个适配的拦截器golangrepo.com/repo/ngrok-…
官方给了几个例子
-
Logging
func (in *sqlInterceptor) StmtQueryContext(ctx context.Context, conn driver.StmtQueryContext, query string, args []driver.NamedValue) (driver.Rows, error) {
startedAt := time.Now()
rows, err := conn.QueryContext(ctx, args)
log.Debug("executed sql query", "duration", time.Since(startedAt), "query", query, "args", args, "err", err)
return rows, err
}
-
Tracing
func (in *sqlInterceptor) StmtQueryContext(ctx context.Context, conn driver.StmtQueryContext, query string, args []driver.NamedValue) (driver.Rows, error) {
span := trace.FromContext(ctx).NewSpan(ctx, "StmtQueryContext")
span.Tags["query"] = query
defer span.Finish()
rows, err := conn.QueryContext(ctx, args)
if err != nil {
span.Error(err)
}
return rows, err
}
-
Retries
func (in *sqlInterceptor) StmtQueryContext(ctx context.Context, conn driver.StmtQueryContext, query string, args []driver.NamedValue) (driver.Rows, error) {
for {
rows, err := conn.QueryContext(ctx, args)
if err == nil {
return rows, nil
}
if err != nil && !isIdempotent(query) {
return nil, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(time.Second):
}
}
}
文档
借鉴