Golang ORM: sqlx

573 阅读3分钟

现状

现有orm依赖库使用beego自带orm包,存在以下问题(gorm也有类似问题)

  1. 日志输出格式不受框架控制,不是json格式,不带trace_id,没有机器实例信息等等
  2. Raw sqlorm混用,风格不统一(复杂查询orm搞不定)
  3. 没有事务相关封装
  4. 没有统一封装log/tracing/metrics等信息,需要手动输出日志
  5. 没有超时控制

现有服务中,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"

}
  1. 单行查询
// 选择某个数据库,可以支持多实例

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

}
  1. 多行查询
// 选择某个数据库,可以支持多实例

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

}
  1. 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()
  1. 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

}
  1. Delete
// 选择某个数据库,可以支持多实例

conn := sqlx.Get(ctx, "db2")

ctx := context.TODO()


u.ID = int(id)

_, err = conn.DeleteContext(ctx, u)

if err != nil {

    return

}
  1. 复杂查询

以上是标准的单表查询,对于复杂的查询,例如连表操作,需要使用类似原生的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

}

// ........
  1. 事务
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-…

官方给了几个例子

  1. 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

}
  1. 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

}
  1. 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):

                }

        }

}

文档

pkg.go.dev/github.com/…

pkg.go.dev/github.com/…

jmoiron.github.io/sqlx/

借鉴

loesspie.com/2020/11/24/…