前言

哈喽,我是asong

明天给大家举荐一个第三方库gendry,这个库是用于辅助操作数据库的Go包。其是基于go-sql-driver/mysql,它提供了一系列的办法来为你调用规范库database/sql中的办法筹备参数。对于我这种不喜爱是应用orm框架的选手,真的是爱不释手,即便不应用orm框架,也能够写出动静sql。上面我就带大家看一看这个库怎么应用!

github地址:https://github.com/didi/gendry

初始化连贯

既然要应用数据库,那么第一步咱们就来进行数据库连贯,咱们先来看一下间接应用规范库进行连贯库是怎么写的:

func NewMysqlClient(conf *config.Server) *sql.DB {    connInfo := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8&parseTime=True&loc=Local", conf.Mysql.Username, conf.Mysql.Password, conf.Mysql.Host, conf.Mysql.Db)    var err error    db, err := sql.Open("mysql", connInfo)    if err != nil {        fmt.Printf("init mysql err %v\n", err)    }    err = db.Ping()    if err != nil {        fmt.Printf("ping mysql err: %v", err)    }    db.SetMaxIdleConns(conf.Mysql.Conn.MaxIdle)    db.SetMaxOpenConns(conf.Mysql.Conn.Maxopen)    db.SetConnMaxLifetime(5 * time.Minute)    fmt.Println("init mysql successc")    return db}

从下面的代码能够看出,咱们须要本人拼接连贯参数,这就须要咱们时刻记住连贯参数(对于我这种记忆白痴,每回都要去度娘一下,很好受)。Gendry为咱们提供了一个manager库,次要用来初始化连接池,设置其各种参数,你能够设置任何go-sql-driver/mysql驱动反对的参数,所以咱们的初始化代码能够这样写:

func MysqlClient(conf *config.Mysql) *sql.DB {    db, err := manager.        New(conf.Db,conf.Username,conf.Password,conf.Host).Set(        manager.SetCharset("utf8"),        manager.SetAllowCleartextPasswords(true),        manager.SetInterpolateParams(true),        manager.SetTimeout(1 * time.Second),        manager.SetReadTimeout(1 * time.Second),            ).Port(conf.Port).Open(true)    if err != nil {        fmt.Printf("init mysql err %v\n", err)    }    err = db.Ping()    if err != nil {        fmt.Printf("ping mysql err: %v", err)    }    db.SetMaxIdleConns(conf.Conn.MaxIdle)    db.SetMaxOpenConns(conf.Conn.Maxopen)    db.SetConnMaxLifetime(5 * time.Minute)    //scanner.SetTagName("json")  // 全局设置,只容许设置一次    fmt.Println("init mysql successc")    return db}

manager做的事件就是帮咱们生成datasourceName,并且它反对了简直所有该驱动反对的参数设置,咱们齐全不须要管datasourceName的格局是怎么的,只管配置参数就能够了。

如何应用?

上面我就带着大家一起来几个demo学习,更多应用办法能够看源代码解锁(之所以没说看官网文档解决的起因:文档不是很具体,还不过看源码来的切实)。

数据库筹备

既然是写示例代码,那么肯定要先有一个数据表来提供测试呀,测试数据表如下:

create table users(    id       bigint unsigned auto_increment        primary key,    username varchar(64)  default '' not null,    nickname varchar(255) default '' null,    password varchar(256) default '' not null,    salt     varchar(48)  default '' not null,    avatar   varchar(128)            null,    uptime   bigint       default 0  not null,    constraint username        unique (username))    charset = utf8mb4;

好了数据表也有了,上面就开始展现吧,以下依照增删改查的程序顺次展现~。

插入数据

gendry提供了三种办法帮忙你结构插入sql,别离是:

// BuildInsert work as its name saysfunc BuildInsert(table string, data []map[string]interface{}) (string, []interface{}, error) {    return buildInsert(table, data, commonInsert)}// BuildInsertIgnore work as its name saysfunc BuildInsertIgnore(table string, data []map[string]interface{}) (string, []interface{}, error) {    return buildInsert(table, data, ignoreInsert)}// BuildReplaceInsert work as its name saysfunc BuildReplaceInsert(table string, data []map[string]interface{}) (string, []interface{}, error) {    return buildInsert(table, data, replaceInsert)}// BuildInsertOnDuplicateKey builds an INSERT ... ON DUPLICATE KEY UPDATE clause.func BuildInsertOnDuplicate(table string, data []map[string]interface{}, update map[string]interface{}) (string, []interface{}, error) {    return buildInsertOnDuplicate(table, data, update)}

看命名想必大家就曾经晓得他们代表的是什么意思了吧,这里就不一一解释了,这里咱们以buildInsert为示例,写一个小demo:

func (db *UserDB) Add(ctx context.Context,cond map[string]interface{}) (int64,error) {    sqlStr,values,err := builder.BuildInsert(tplTable,[]map[string]interface{}{cond})    if err != nil{        return 0,err    }    // TODO:DEBUG    fmt.Println(sqlStr,values)    res,err := db.cli.ExecContext(ctx,sqlStr,values...)    if err != nil{        return 0,err    }    return res.LastInsertId()}// 单元测试如下:func (u *UserDBTest) Test_Add()  {    cond := map[string]interface{}{        "username": "test_add",        "nickname": "asong",        "password": "123456",        "salt": "oooo",        "avatar": "http://www.baidu.com",        "uptime": 123,    }    s,err := u.db.Add(context.Background(),cond)    u.Nil(err)    u.T().Log(s)}

咱们把要插入的数据放到map构造中,key就是要字段,value就是咱们要插入的值,其余都交给 builder.BuildInsert就好了,咱们的代码大大减少。大家必定很好奇这个办法是怎么实现的呢?别着急,前面咱们一起解密。

删除数据

我最喜爱删数据了,不晓得为什么,删完数据总有一种快感。。。。

删除数据能够间接调用 builder.BuildDelete办法,比方咱们当初咱们要删除方才插入的那条数据:

func (db *UserDB)Delete(ctx context.Context,where map[string]interface{}) error {    sqlStr,values,err := builder.BuildDelete(tplTable,where)    if err != nil{        return err    }    // TODO:DEBUG    fmt.Println(sqlStr,values)    res,err := db.cli.ExecContext(ctx,sqlStr,values...)    if err != nil{        return err    }    affectedRows,err := res.RowsAffected()    if err != nil{        return err    }    if affectedRows == 0{        return errors.New("no record delete")    }    return nil}// 单测如下:func (u *UserDBTest)Test_Delete()  {    where := map[string]interface{}{        "username in": []string{"test_add"},    }    err := u.db.Delete(context.Background(),where)    u.Nil(err)}

这里在传入where条件时,key应用的username in,这里应用空格加了一个操作符in,这是gendry库所反对的写法,当咱们的SQL存在一些操作符时,就能够通过这样办法进行书写,模式如下:

where := map[string]interface{}{    "field 操作符": "value",}

官文文档给出的反对操作如下:

=><=<=>=!=<>innot inlikenot likebetweennot between

既然说到了这里,顺便把gendry反对的关键字也说一下吧,官网文档给出的反对如下:

_or_orderby_groupby_having_limit_lockMode

参考示例:

where := map[string]interface{}{    "age >": 100,    "_or": []map[string]interface{}{        {            "x1":    11,            "x2 >=": 45,        },        {            "x3":    "234",            "x4 <>": "tx2",        },    },    "_orderby": "fieldName asc",    "_groupby": "fieldName",    "_having": map[string]interface{}{"foo":"bar",},    "_limit": []uint{offset, row_count},    "_lockMode": "share",}

这里有几个须要留神的问题:

  • 如果_groupby没有被设置将疏忽_having
  • _limit能够这样写:

    • "_limit": []uint{a,b} => LIMIT a,b
    • "_limit": []uint{a} => LIMIT 0,a
  • _lockMode临时只反对shareexclusive

    • share代表的是SELECT ... LOCK IN SHARE MODE.可怜的是,以后版本不反对SELECT ... FOR SHARE.
    • exclusive代表的是SELECT ... FOR UPDATE.

更新数据

更新数据能够应用builder.BuildUpdate办法进行构建sql语句,不过要留神的是,他不反对_orderby_groupby_having.只有这个是咱们所须要留神的,其余的失常应用就能够了。

func (db *UserDB) Update(ctx context.Context,where map[string]interface{},data map[string]interface{}) error {    sqlStr,values,err := builder.BuildUpdate(tplTable,where,data)    if err != nil{        return err    }    // TODO:DEBUG    fmt.Println(sqlStr,values)    res,err := db.cli.ExecContext(ctx,sqlStr,values...)    if err != nil{        return err    }    affectedRows,err := res.RowsAffected()    if err != nil{        return err    }    if affectedRows == 0{        return errors.New("no record update")    }    return nil}// 单元测试如下:func (u *UserDBTest) Test_Update()  {    where := map[string]interface{}{        "username": "asong",    }    data := map[string]interface{}{        "nickname": "shuai",    }    err := u.db.Update(context.Background(),where,data)    u.Nil(err)}

这里入参变成了两个,一个是用来指定where条件的,另一个就是来放咱们要更新的数据的。

查问数据

查问应用的是builder.BuildSelect办法来构建sql语句,先来一个示例,看看怎么用?

func (db *UserDB) Query(ctx context.Context,cond map[string]interface{}) ([]*model.User,error) {    sqlStr,values,err := builder.BuildSelect(tplTable,cond,db.getFiledList())    if err != nil{        return nil, err    }    rows,err := db.cli.QueryContext(ctx,sqlStr,values...)    defer func() {        if rows != nil{            _ = rows.Close()        }    }()    if err != nil{        if err == sql.ErrNoRows{            return nil,errors.New("not found")        }        return nil,err    }    user := make([]*model.User,0)    err = scanner.Scan(rows,&user)    if err != nil{        return nil,err    }    return user,nil}// 单元测试func (u *UserDBTest) Test_Query()  {    cond := map[string]interface{}{        "id in": []int{1,2},    }    s,err := u.db.Query(context.Background(),cond)    u.Nil(err)    for k,v := range s{        u.T().Log(k,v)    }}

BuildSelect(table string, where map[string]interface{}, selectField []string)总共有三个入参,table就是数据表名,where外面就是咱们的条件参数,selectFiled就是咱们要查问的字段,如果传nil,对应的sql语句就是select * ...。看完下面的代码,零碎的敌人应该会对scanner.Scan,这个就是gendry提供一个映射后果集的办法,上面咱们来看一看这个库怎么用。

scanner

执行了数据库操作之后,要把返回的后果集和自定义的struct进行映射。Scanner提供一个简略的接口通过反射来进行后果集和自定义类型的绑定,下面的scanner.Scan办法就是来做这个,scanner进行反射时会应用构造体的tag。默认应用的tagName是ddb:"xxx",你也能够自定义。应用scanner.SetTagName("json")进行设置,scaner.SetTagName是全局设置,为了防止歧义,只容许设置一次,个别在初始化DB阶段进行此项设置.

有时候咱们可能不太想定义一个构造体去存两头后果,那么gendry还提供了scanMap能够应用:

rows,_ := db.Query("select name,m_age from person")result,err := scanner.ScanMap(rows)for _,record := range result {    fmt.Println(record["name"], record["m_age"])}

在应用scanner是有以下几点须要留神:

  • 如果是应用Scan或者ScanMap的话,你必须在之后手动close rows
  • 传给Scan的必须是援用
  • ScanClose和ScanMapClose不须要手动close rows

手写SQL

对于一些比较复杂的查问,gendry办法就不能满足咱们的需要了,这就可能须要咱们自定义sql了,gendry提供了NamedQuery就是这么应用的,具体应用如下:

func (db *UserDB) CustomizeGet(ctx context.Context,sql string,data map[string]interface{}) (*model.User,error) {    sqlStr,values,err := builder.NamedQuery(sql,data)    if err != nil{        return nil, err    }    // TODO:DEBUG    fmt.Println(sql,values)    rows,err := db.cli.QueryContext(ctx,sqlStr,values...)    if err != nil{        return nil,err    }    defer func() {        if rows != nil{            _ = rows.Close()        }    }()    user := model.NewEmptyUser()    err = scanner.Scan(rows,&user)    if err != nil{        return nil,err    }    return user,nil}// 单元测试func (u *UserDBTest) Test_CustomizeGet()  {    sql := "SELECT * FROM users WHERE username={{username}}"    data := map[string]interface{}{        "username": "test_add",    }    user,err := u.db.CustomizeGet(context.Background(),sql,data)    u.Nil(err)    u.T().Log(user)}

这种就是纯手写sql了,一些简单的中央能够这么应用。

聚合查问

gendry还为咱们提供了聚合查问,例如:count,sum,max,min,avg。这里就拿count来举例吧,假如咱们当初要统计明码雷同的用户有多少,就能够这么写:

func (db *UserDB) AggregateCount(ctx context.Context,where map[string]interface{},filed string) (int64,error) {    res,err := builder.AggregateQuery(ctx,db.cli,tplTable,where,builder.AggregateCount(filed))    if err != nil{        return 0, err    }    numberOfRecords := res.Int64()    return numberOfRecords,nil}// 单元测试func (u *UserDBTest) Test_AggregateCount()  {    where := map[string]interface{}{        "password": "123456",    }    count,err := u.db.AggregateCount(context.Background(),where,"*")    u.Nil(err)    u.T().Log(count)}

到这里,所有的根本用法根本演示了一遍,更多的应用办法能够自行解锁。

cli工具

除了下面这些API以外,Gendry还提供了一个命令行来进行代码生成,能够显著缩小你的开发量,gforge是基于gendry的cli工具,它依据表名生成golang构造,这能够加重您的累赘。甚至gforge都能够为您生成残缺的DAO层。

装置

go get -u github.com/caibirdme/gforge

应用gforge -h来验证是否装置胜利,同时会给出应用提醒。

生成表构造

应用gforge生成的表构造是能够通过golint govet的。生成指令如下:

gforge table -uroot -proot1997 -h127.0.0.1 -dasong -tusers// Users is a mapping object for users table in mysqltype Users struct {    ID uint64 `json:"id"`    Username string `json:"username"`    Nickname string `json:"nickname"`    Password string `json:"password"`    Salt string `json:"salt"`    Avatar string `json:"avatar"`    Uptime int64 `json:"uptime"`}

这样就省去了咱们自定义表构造的工夫,或者更不便的是间接把dao层生成进去。

生成dao文件

运行指令如下:

gforge dao -uroot -proot1997 -h127.0.0.1 -dasong -tusers | gofmt > dao.go

这里我把生成的dao层间接丢到了文件里了,这里就不贴具体代码了,没有意义,晓得怎么应用就好了。

解密

想必大家肯定都跟我一样特地好奇gendry是怎么实现的呢?上面就以builder.buildSelect为例子,咱们来看一看他是怎么实现的。其余原理类似,有趣味的童鞋能够看源码学习。咱们先来看一下buildSelect这个办法的源码:

func BuildSelect(table string, where map[string]interface{}, selectField []string) (cond string, vals []interface{}, err error) {    var orderBy string    var limit *eleLimit    var groupBy string    var having map[string]interface{}    var lockMode string    if val, ok := where["_orderby"]; ok {        s, ok := val.(string)        if !ok {            err = errOrderByValueType            return        }        orderBy = strings.TrimSpace(s)    }    if val, ok := where["_groupby"]; ok {        s, ok := val.(string)        if !ok {            err = errGroupByValueType            return        }        groupBy = strings.TrimSpace(s)        if "" != groupBy {            if h, ok := where["_having"]; ok {                having, err = resolveHaving(h)                if nil != err {                    return                }            }        }    }    if val, ok := where["_limit"]; ok {        arr, ok := val.([]uint)        if !ok {            err = errLimitValueType            return        }        if len(arr) != 2 {            if len(arr) == 1 {                arr = []uint{0, arr[0]}            } else {                err = errLimitValueLength                return            }        }        begin, step := arr[0], arr[1]        limit = &eleLimit{            begin: begin,            step:  step,        }    }    if val, ok := where["_lockMode"]; ok {        s, ok := val.(string)        if !ok {            err = errLockModeValueType            return        }        lockMode = strings.TrimSpace(s)        if _, ok := allowedLockMode[lockMode]; !ok {            err = errNotAllowedLockMode            return        }    }    conditions, err := getWhereConditions(where, defaultIgnoreKeys)    if nil != err {        return    }    if having != nil {        havingCondition, err1 := getWhereConditions(having, defaultIgnoreKeys)        if nil != err1 {            err = err1            return        }        conditions = append(conditions, nilComparable(0))        conditions = append(conditions, havingCondition...)    }    return buildSelect(table, selectField, groupBy, orderBy, lockMode, limit, conditions...)}
  • 首先会对几个关键字进行解决。
  • 而后会调用getWhereConditions这个办法去结构sql,看一下外部实现(摘取局部):
for key, val := range where {        if _, ok := ignoreKeys[key]; ok {            continue        }        if key == "_or" {            var (                orWheres          []map[string]interface{}                orWhereComparable []Comparable                ok                bool            )            if orWheres, ok = val.([]map[string]interface{}); !ok {                return nil, errOrValueType            }            for _, orWhere := range orWheres {                if orWhere == nil {                    continue                }                orNestWhere, err := getWhereConditions(orWhere, ignoreKeys)                if nil != err {                    return nil, err                }                orWhereComparable = append(orWhereComparable, NestWhere(orNestWhere))            }            comparables = append(comparables, OrWhere(orWhereComparable))            continue        }        field, operator, err = splitKey(key)        if nil != err {            return nil, err        }        operator = strings.ToLower(operator)        if !isStringInSlice(operator, opOrder) {            return nil, ErrUnsupportedOperator        }        if _, ok := val.(NullType); ok {            operator = opNull        }        wms.add(operator, field, val)    }

这一段就是遍历slice,之前解决过的关键字局部会被疏忽,_or关键字会递归解决失去所有条件数据。之后就没有特地要阐明的中央了。我本人返回到buildSelect办法中,在解决了where条件之后,如果有having条件还会在进行一次过滤,最初所有的数据构建好了后,会调用buildSelect办法来结构最初的sql语句。

总结

看过源码当前,只想说:大佬就是大佬。源码其实很容易看懂,这就没有做具体的解析,次要是这样思维值得大家学习,倡议大家都能够看一遍gendry的源码,涨常识~~。

好啦,这篇文章就到这里啦,素质三连(分享、点赞、在看)都是笔者继续创作更多优质内容的能源!

建了一个Golang交换群,欢送大家的退出,第一工夫观看优质文章,不容错过哦(公众号获取)

结尾给大家发一个小福利吧,最近我在看[微服务架构设计模式]这一本书,讲的很好,本人也收集了一本PDF,有须要的小伙能够到自行下载。获取形式:关注公众号:[Golang梦工厂],后盾回复:[微服务],即可获取。

我翻译了一份GIN中文文档,会定期进行保护,有须要的小伙伴后盾回复[gin]即可下载。

翻译了一份Machinery中文文档,会定期进行保护,有须要的小伙伴们后盾回复[machinery]即可获取。

我是asong,一名普普通通的程序猿,让gi我一起缓缓变强吧。我本人建了一个golang交换群,有须要的小伙伴加我vx,我拉你入群。欢送各位的关注,咱们下期见~~~

举荐往期文章:

  • machinery-go异步工作队列
  • Leaf—Segment分布式ID生成零碎(Golang实现版本)
  • 十张动图带你搞懂排序算法(附go实现代码)
  • Go语言相干书籍举荐(从入门到放弃)
  • go参数传递类型
  • 手把手教姐姐写音讯队列
  • 常见面试题之缓存雪崩、缓存穿透、缓存击穿
  • 详解Context包,看这一篇就够了!!!
  • go-ElasticSearch入门看这一篇就够了(一)
  • 面试官:go中for-range应用过吗?这几个问题你能解释一下起因吗
  • 学会wire依赖注入、cron定时工作其实就这么简略!
  • 据说你还不会jwt和swagger-饭我都不吃了带着实际我的项目我就来了