关于golang:sqlx操作MySQL实战及其原理

36次阅读

共计 8567 个字符,预计需要花费 22 分钟才能阅读完成。

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=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=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-…

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

正文完
 0