動態sql工具之gendry

前言

哈嘍,我是 asong

今天給你們推薦一個第三方庫gendry,這個庫是用於輔助操做數據庫的Go包。其是基於go-sql-driver/mysql,它提供了一系列的方法來爲你調用標準庫database/sql中的方法準備參數。對於我這種不喜歡是使用orm框架的選手,真的是愛不釋手,即便不使用orm框架,也能夠寫出動態sql。下面我就帶你們看一看這個庫怎麼使用!mysql

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

初始化鏈接

既然要使用數據庫,那麼第一步咱們就來進行數據庫鏈接,咱們先來看一下直接使用標準庫進行鏈接庫是怎樣寫的:github

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驅動支持的參數,因此咱們的初始化代碼能夠這樣寫:golang

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學習,更多使用方法能夠看源代碼解鎖(之因此沒說看官方文檔解決的緣由:文檔不是很詳細,還不過看源碼來的實在)。算法

數據庫準備

既然是寫示例代碼,那麼必定要先有一個數據表來提供測試呀,測試數據表以下:sql

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,分別是:json

// BuildInsert work as its name says
func BuildInsert(table string, data []map[string]interface{}) (string, []interface{}, error) {
    return buildInsert(table, data, commonInsert)
}

// BuildInsertIgnore work as its name says
func BuildInsertIgnore(table string, data []map[string]interface{}) (string, []interface{}, error) {
    return buildInsert(table, data, ignoreInsert)
}

// BuildReplaceInsert work as its name says
func 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",
}

官文文檔給出的支持操做以下:

=
>
<
=
<=
>=
!=
<>
in
not in
like
not like
between
not 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 mysql
type 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,我拉你入羣。歡迎各位的關注,咱們下期見~~~

推薦往期文章:

相關文章
相關標籤/搜索