sqlx是Golang中的一个出名三方库,其为Go规范库database/sql提供了一组扩大反对。应用它能够不便的在数据行与Golang的构造体、映射和切片之间进行转换,从这个角度能够说它是一个ORM框架;它还封装了一系列地罕用SQL操作方法,让咱们用起来更爽。

sqlx实战

这里以操作MySQL的增删改查为例。

筹备工作

先要筹备一个MySQL,这里通过docker疾速启动一个MySQL 5.7。

docker run -d --name mysql1 -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 mysql:5.7

在MySQL中创立一个名为test的数据库:

CREATE DATABASE `test` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;

数据库中创立一个名为Person的数据库表:

CREATE TABLE test.Person (    Id integer auto_increment NOT NULL,    Name VARCHAR(30) NULL,    City VARCHAR(50) NULL,    AddTime DATETIME NOT NULL,    UpdateTime DATETIME NOT NULL,    CONSTRAINT Person_PK PRIMARY KEY (Id))ENGINE=InnoDBDEFAULT CHARSET=utf8mb4COLLATE=utf8mb4_general_ci;

而后创立一个Go我的项目,装置sqlx:

go get github.com/jmoiron/sqlx

因为操作的是MySQL,还须要装置MySQL的驱动:

go get github.com/go-sql-driver/mysql

编写代码

增加援用

增加sqlx和mysql驱动的援用:

import (    "log"    _ "github.com/go-sql-driver/mysql"    "github.com/jmoiron/sqlx")

MySQL的驱动是隐式注册的,并不会在接下来的程序中间接调用,所以这里加了下划线。

创立连贯

操作数据库前须要先创立一个连贯:

    db, err := sqlx.Connect("mysql", "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=true&loc=Local")    if err != nil {        log.Println("数据库连贯失败")    }

这个连贯中指定了程序要用MySQL驱动,以及MySQL的连贯地址、用户名和明码、数据库名称、字符编码方式;这里还有两个参数parseTime和loc,parseTime的作用是让MySQL中工夫类型的值能够映射到Golang中的time.Time类型,loc的作用是设置time.Time的值的时区为以后零碎时区,不应用这个参数的话保留到的数据库的就是UTC工夫,会和北京时间差8个小时。

增删改查

sqlx扩大了DB和Tx,继承了它们原有的办法,并扩大了一些办法,这里次要看下这些扩大的办法。

减少

通用占位符的形式:

insertResult := db.MustExec("INSERT INTO Person (Name, City, AddTime, UpdateTime) VALUES (?, ?, ?, ?)", "Zhang San", "Beijing", time.Now(), time.Now())lastInsertId, _ := insertResult.LastInsertId()log.Println("Insert Id is ", lastInsertId)

这个表的主键应用了自增的形式,能够通过返回值的LastInsertId办法获取。

命名参数的形式:

insertPerson := &Person{        Name:       "Li Si",        City:       "Shanghai",        AddTime:    time.Now(),        UpdateTime: time.Now(),    }    insertPersonResult, err := db.NamedExec("INSERT INTO Person (Name, City, AddTime, UpdateTime) VALUES(:Name, :City, :AddTime, :UpdateTime)", insertPerson)

命名参数的形式是sqlx扩大的,这个形式就是常说的ORM。这里须要留神给struct字段增加上db标签:

type Person struct {    Id         int       `db:"Id"`    Name       string    `db:"Name"`    City       string    `db:"City"`    AddTime    time.Time `db:"AddTime"`    UpdateTime time.Time `db:"UpdateTime"`}

struct中的字段名称不用和数据库字段雷同,只须要通过db标签映射正确就行。留神SQL语句中应用的命名参数须要是db标签中的名字。

除了能够映射struct,sqlx还反对map,请看上面这个示例:

insertMap := map[string]interface{}{        "n": "Wang Wu",        "c": "HongKong",        "a": time.Now(),        "u": time.Now(),    }    insertMapResult, err := db.NamedExec("INSERT INTO Person (Name, City, AddTime, UpdateTime) VALUES(:n, :c, :a, :u)", insertMap)

再来看看批减少的形式:

insertPersonArray := []Person{        {Name: "BOSIMA", City: "Wu Han", AddTime: time.Now(), UpdateTime: time.Now()},        {Name: "BOSSMA", City: "Xi An", AddTime: time.Now(), UpdateTime: time.Now()},        {Name: "BOMA", City: "Cheng Du", AddTime: time.Now(), UpdateTime: time.Now()},    }    insertPersonArrayResult, err := db.NamedExec("INSERT INTO Person (Name, City, AddTime, UpdateTime) VALUES(:Name, :City, :AddTime, :UpdateTime)", insertPersonArray)    if err != nil {        log.Println(err)        return    }    insertPersonArrayId, _ := insertPersonArrayResult.LastInsertId()    log.Println("InsertPersonArray Id is ", insertPersonArrayId)

这里还是采纳命名参数的形式,参数传递一个struct数组或者切片就能够了。这个执行后果中也能够获取到最初插入数据的自增Id,不过实测返回的是本次插入的第一条的Id,这个有点顺当,然而思考到减少多条只获取一个Id的场景仿佛没有,所以也不必多虑。

除了应用struct数组或切片,也能够应用map数组或切片,这里就不贴出来了,有趣味的能够去看文末给出的Demo链接。

删除

删除也能够应用通用占位符和命名参数的形式,并且会返回本次执行受影响的行数,某些状况下能够应用这个数字判断SQL理论有没有执行胜利。

deleteResult := db.MustExec("Delete from Person where Id=?", 1)log.Println(deleteResult.RowsAffected())deleteMapResult, err := db.NamedExec("Delete from Person where Id=:Id",                                     map[string]interface{}{"Id": 1})if err != nil {  log.Println(err)  return}log.Println(deleteMapResult.RowsAffected())

批改

Sqlx对批改的反对和删除差不多:

updateResult := db.MustExec("Update Person set City=?, UpdateTime=? where Id=?", "Shanghai", time.Now(), 1)log.Println(updateResult.RowsAffected())updateMapResult, err := db.NamedExec("Update Person set City=:City, UpdateTime=:UpdateTime where Id=:Id",                                     map[string]interface{}{"City": "Chong Qing", "UpdateTime": time.Now(), "Id": 1})if err != nil {  log.Println(err)}log.Println(updateMapResult.RowsAffected())

查问

Sqlx对查问的反对比拟多。

应用Get办法查问一条:

getPerson := &Person{}db.Get(getPerson, "select * from Person where Name=?", "Zhang San")

应用Select办法查问多条:

selectPersons := []Person{}db.Select(&selectPersons, "select * from Person where Name=?", "Zhang San")

只查问局部字段:

getId := new(int64)db.Get(getId, "select Id from Person where Name=?", "Zhang San")selectTowFieldSlice := []Person{}db.Select(&selectTowFieldSlice, "select Id,Name from Person where Name=?", "Zhang San")selectNameSlice := []string{}db.Select(&selectNameSlice, "select Name from Person where Name=?", "Zhang San")

从上能够看出如果只查问局部字段,还能够持续应用struct;特地的只查问一个字段时,应用根本数据类型就能够了。

除了这些高层次的形象办法,Sqlx也对更低层次的查询方法进行了扩大:

查问单行:

row = db.QueryRowx("select * from Person where Name=?", "Zhang San")    if row.Err() == sql.ErrNoRows {        log.Println("Not found Zhang San")    } else {        queryPerson := &Person{}        err = row.StructScan(queryPerson)        if err != nil {            log.Println(err)            return        }        log.Println("QueryRowx-StructScan:", queryPerson.City)    }

查问多行:

    rows, err := db.Queryx("select * from Person where Name=?", "Zhang San")    if err != nil {        log.Println(err)        return    }    for rows.Next() {        rowSlice, err := rows.SliceScan()        if err != nil {            log.Println(err)            return        }        log.Println("Queryx-SliceScan:", string(rowSlice[2].([]byte)))    }

命名参数Query:

rows, err = db.NamedQuery("select * from Person where Name=:n", map[string]interface{}{"n": "Zhang San"})

查问出数据行后,这里有多种映射办法:StructScan、SliceScan和MapScan,别离对应映射后的不同数据结构。

预处理语句

对于重复使用的SQL语句,能够采纳预处理的形式,缩小SQL解析的次数,缩小网络通信量,从而进步SQL操作的吞吐量。

上面的代码展现了sqlx中如何应用stmt查问数据,别离采纳了命名参数和通用占位符两种传参形式。

bosima := Person{}bossma := Person{}nstmt, err := db.PrepareNamed("SELECT * FROM Person WHERE Name = :n")if err != nil {  log.Println(err)  return}err = nstmt.Get(&bossma, map[string]interface{}{"n": "BOSSMA"})if err != nil {  log.Println(err)  return}log.Println("NamedStmt-Get1:", bossma.City)err = nstmt.Get(&bosima, map[string]interface{}{"n": "BOSIMA"})if err != nil {  log.Println(err)  return}log.Println("NamedStmt-Get2:", bosima.City)stmt, err := db.Preparex("SELECT * FROM Person WHERE Name=?")if err != nil {  log.Println(err)  return}err = stmt.Get(&bosima, "BOSIMA")if err != nil {  log.Println(err)  return}log.Println("Stmt-Get1:", bosima.City)err = stmt.Get(&bossma, "BOSSMA")if err != nil {  log.Println(err)  return}log.Println("Stmt-Get2:", bossma.City)

对于上文增删改查的办法,sqlx都有相应的扩大办法。与上文不同的是,须要先应用SQL模版创立一个stmt实例,而后执行相干SQL操作时,不再须要传递SQL语句。

数据库事务

为了在事务中执行sqlx扩大的增删改查办法,sqlx必然也对数据库事务做一些必要的扩大反对。

tx, err = db.Beginx()    if err != nil {        log.Println(err)        return    }    tx.MustExec("INSERT INTO Person (Name, City, AddTime, UpdateTime) VALUES (?, ?, ?, ?)", "Zhang San", "Beijing", time.Now(), time.Now())    tx.MustExec("INSERT INTO Person (Name, City, AddTime, UpdateTime) VALUES (?, ?, ?, ?)", "Li Si Hai", "Dong Bei", time.Now(), time.Now())    err = tx.Commit()    if err != nil {        log.Println(err)        return    }    log.Println("tx-Beginx is successful")

下面这段代码就是一个简略的sqlx数据库事务示例,先通过db.Beginx开启事务,而后执行SQL语句,最初提交事务。

如果想要更改默认的数据库隔离级别,能够应用另一个扩大办法:

tx, err = db.BeginTxx(context.Background(), &sql.TxOptions{Isolation: sql.LevelRepeatableRead})

sqlx干了什么

通过上边的实战,基本上就能够应用sqlx进行开发了。为了更好的应用sqlx,咱们能够再理解下sqlx是怎么做到上边这些扩大的。

Go的规范库中没有提供任何具体数据库的驱动,只是通过database/sql库定义了操作数据库的通用接口。sqlx中也没有蕴含具体数据库的驱动,它只是封装了罕用SQL的操作方法,让咱们的SQL写起来更爽。

MustXXX

sqlx提供两个几个MustXXX办法。

Must办法是为了简化错误处理而呈现的,当开发者确定SQL操作不会返回谬误的时候就能够应用Must办法,然而如果真的呈现了未知谬误的时候,这个办法外部会触发panic,开发者须要有一个兜底的计划来解决这个panic,比方应用recover。

这里是MustExec的源码:

func MustExec(e Execer, query string, args ...interface{}) sql.Result {    res, err := e.Exec(query, args...)    if err != nil {        panic(err)    }    return res}

NamedXXX

对于须要传递SQL参数的办法, sqlx都扩大了命名参数的传参形式。这让咱们能够在更高的抽象层次解决数据库操作,而不用关怀数据库操作的细节。

这种办法的外部会解析咱们的SQL语句,而后从传递的struct、map或者slice中提取命名参数对应的值,而后造成新的SQL语句和参数汇合,再交给底层database/sql的办法去执行。

这里摘抄一些代码:

func NamedExec(e Ext, query string, arg interface{}) (sql.Result, error) {    q, args, err := bindNamedMapper(BindType(e.DriverName()), query, arg, mapperFor(e))    if err != nil {        return nil, err    }    return e.Exec(q, args...)}

NamedExec 外部调用了 bindNamedMapper,这个办法就是用于提取参数值的。其外部别离对Map、Slice和Struct有不同的解决。

func bindNamedMapper(bindType int, query string, arg interface{}, m *reflectx.Mapper) (string, []interface{}, error) {    ...    switch {    case k == reflect.Map && t.Key().Kind() == reflect.String:        ...        return bindMap(bindType, query, m)    case k == reflect.Array || k == reflect.Slice:        return bindArray(bindType, query, arg, m)    default:        return bindStruct(bindType, query, arg, m)    }}

以批量插入为例,咱们的代码是这样写的:

insertPersonArray := []Person{        {Name: "BOSIMA", City: "Wu Han", AddTime: time.Now(), UpdateTime: time.Now()},        {Name: "BOSSMA", City: "Xi An", AddTime: time.Now(), UpdateTime: time.Now()},        {Name: "BOMA", City: "Cheng Du", AddTime: time.Now(), UpdateTime: time.Now()},    }    insertPersonArrayResult, err := db.NamedExec("INSERT INTO Person (Name, City, AddTime, UpdateTime) VALUES(:Name, :City, :AddTime, :UpdateTime)", insertPersonArray)    

通过bindNamedMapper解决后SQL语句和参数是这样的:

这里应用了反射,有些人可能会放心性能的问题,对于这个问题的常见解决形式就是缓存起来,sqlx也是这样做的。

XXXScan

这些Scan办法让数据行到对象的映射更为不便,sqlx提供了StructScan、SliceScan和MapScan,看名字就能够晓得它们映射的数据结构。而且在这些映射能力的根底上,sqlx提供了更为形象的Get和Select办法。

这些Scan外部还是调用了database/sql的Row.Scan办法。

以StructScan为例,其应用办法为:

queryPerson := &Person{}err = row.StructScan(queryPerson)

通过sqlx解决后,调用Row.Scan的参数是:


以上就是本文的次要内容,如有错漏,欢送斧正。

老规矩,Demo程序曾经上传到Github,欢送拜访:https://github.com/bosima/go-...

播种更多架构常识,请关注微信公众号 萤火架构。原创内容,转载请注明出处。