前言
哈喽,我是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
临时只反对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
提供一个映射后果集的办法,上面咱们来看一看这个库怎么用。
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-饭我都不吃了带着实际我的项目我就来了