哈嘍,我是asong
。今天給你們推薦一個第三方庫
gendry
,這個庫是用於輔助操做數據庫的Go
包。其是基於go-sql-driver/mysql
,它提供了一系列的方法來爲你調用標準庫database/sql
中的方法準備參數。對於我這種不喜歡是使用orm
框架的選手,真的是愛不釋手,即便不使用orm
框架,也能夠寫出動態sql
。下面我就帶你們看一看這個庫怎麼使用!mysqlgithub地址: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
暫時只支持share
和exclusive
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
提供一個映射結果集的方法,下面咱們來看一看這個庫怎麼用。
執行了數據庫操做以後,要把返回的結果集和自定義的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
是有如下幾點須要注意:
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) }
到這裏,全部的基本用法基本演示了一遍,更多的使用方法能夠自行解鎖。
除了上面這些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
,我拉你入羣。歡迎各位的關注,咱們下期見~~~
推薦往期文章: