Go语言操作MySQL

MySQL是业界常用的关系型数据库,本文介绍了Go语言如何操作MySQL数据库。

Go操作MySQL

连接

Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的数据库驱动。使用database/sql包时必须注入(至少)一个数据库驱动。

我们常用的数据库基本上都有完整的第三方实现。例如:MySQL驱动

下载依赖

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

使用MySQL驱动

  1. func Open(driverName, dataSourceName string) (*DB, error)

Open打开一个dirverName指定的数据库,dataSourceName指定数据源,一般至少包括数据库文件名和其它连接必要的信息。

  1. import (
  2. "database/sql"
  3. _ "github.com/go-sql-driver/mysql"
  4. )
  5. func main() {
  6. // DSN:Data Source Name
  7. dsn := "user:password@tcp(127.0.0.1:3306)/dbname"
  8. db, err := sql.Open("mysql", dsn)
  9. if err != nil {
  10. panic(err)
  11. }
  12. defer db.Close() // 注意这行代码要写在上面err判断的下面
  13. }

思考题: 为什么上面代码中的defer db.Close()语句不应该写在if err != nil的前面呢?

初始化连接

Open函数可能只是验证其参数格式是否正确,实际上并不创建与数据库的连接。如果要检查数据源的名称是否真实有效,应该调用Ping方法。

返回的DB对象可以安全地被多个goroutine并发使用,并且维护其自己的空闲连接池。因此,Open函数应该仅被调用一次,很少需要关闭这个DB对象。

  1. // 定义一个全局对象db
  2. var db *sql.DB
  3. // 定义一个初始化数据库的函数
  4. func initDB() (err error) {
  5. // DSN:Data Source Name
  6. dsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"
  7. // 不会校验账号密码是否正确
  8. // 注意!!!这里不要使用:=,我们是给全局变量赋值,然后在main函数中使用全局变量db
  9. db, err = sql.Open("mysql", dsn)
  10. if err != nil {
  11. return err
  12. }
  13. // 尝试与数据库建立连接(校验dsn是否正确)
  14. err = db.Ping()
  15. if err != nil {
  16. return err
  17. }
  18. return nil
  19. }
  20. func main() {
  21. err := initDB() // 调用输出化数据库的函数
  22. if err != nil {
  23. fmt.Printf("init db failed,err:%v\n", err)
  24. return
  25. }
  26. }

其中sql.DB是表示连接的数据库对象(结构体实例),它保存了连接数据库相关的所有信息。它内部维护着一个具有零到多个底层连接的连接池,它可以安全地被多个goroutine同时使用。

SetMaxOpenConns

  1. func (db *DB) SetMaxOpenConns(n int)

SetMaxOpenConns设置与数据库建立连接的最大数目。 如果n大于0且小于最大闲置连接数,会将最大闲置连接数减小到匹配最大开启连接数的限制。 如果n<=0,不会限制最大开启连接数,默认为0(无限制)。

SetMaxIdleConns

  1. func (db *DB) SetMaxIdleConns(n int)

SetMaxIdleConns设置连接池中的最大闲置连接数。 如果n大于最大开启连接数,则新的最大闲置连接数会减小到匹配最大开启连接数的限制。 如果n<=0,不会保留闲置连接。

CRUD

建库建表

我们先在MySQL中创建一个名为sql_test的数据库

  1. CREATE DATABASE sql_test;

进入该数据库:

  1. use sql_test;

执行以下命令创建一张用于测试的数据表:

  1. CREATE TABLE `user` (
  2. `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  3. `name` VARCHAR(20) DEFAULT '',
  4. `age` INT(11) DEFAULT '0',
  5. PRIMARY KEY(`id`)
  6. )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

查询

为了方便查询,我们事先定义好一个结构体来存储user表的数据。

  1. type user struct {
  2. id int
  3. age int
  4. name string
  5. }

单行查询

单行查询db.QueryRow()执行一次查询,并期望返回最多一行结果(即Row)。QueryRow总是返回非nil的值,直到返回值的Scan方法被调用时,才会返回被延迟的错误。(如:未找到结果)

  1. func (db *DB) QueryRow(query string, args ...interface{}) *Row

具体示例代码:

  1. // 查询单条数据示例
  2. func queryRowDemo() {
  3. sqlStr := "select id, name, age from user where id=?"
  4. var u user
  5. // 非常重要:确保QueryRow之后调用Scan方法,否则持有的数据库链接不会被释放
  6. err := db.QueryRow(sqlStr, 1).Scan(&u.id, &u.name, &u.age)
  7. if err != nil {
  8. fmt.Printf("scan failed, err:%v\n", err)
  9. return
  10. }
  11. fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
  12. }

多行查询

多行查询db.Query()执行一次查询,返回多行结果(即Rows),一般用于执行select命令。参数args表示query中的占位参数。

  1. func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

具体示例代码:

  1. // 查询多条数据示例
  2. func queryMultiRowDemo() {
  3. sqlStr := "select id, name, age from user where id > ?"
  4. rows, err := db.Query(sqlStr, 0)
  5. if err != nil {
  6. fmt.Printf("query failed, err:%v\n", err)
  7. return
  8. }
  9. // 非常重要:关闭rows释放持有的数据库链接
  10. defer rows.Close()
  11. // 循环读取结果集中的数据
  12. for rows.Next() {
  13. var u user
  14. err := rows.Scan(&u.id, &u.name, &u.age)
  15. if err != nil {
  16. fmt.Printf("scan failed, err:%v\n", err)
  17. return
  18. }
  19. fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
  20. }
  21. }

插入数据

插入、更新和删除操作都使用Exec方法。

  1. func (db *DB) Exec(query string, args ...interface{}) (Result, error)

Exec执行一次命令(包括查询、删除、更新、插入等),返回的Result是对已执行的SQL命令的总结。参数args表示query中的占位参数。

具体插入数据示例代码如下:

  1. // 插入数据
  2. func insertRowDemo() {
  3. sqlStr := "insert into user(name, age) values (?,?)"
  4. ret, err := db.Exec(sqlStr, "王五", 38)
  5. if err != nil {
  6. fmt.Printf("insert failed, err:%v\n", err)
  7. return
  8. }
  9. theID, err := ret.LastInsertId() // 新插入数据的id
  10. if err != nil {
  11. fmt.Printf("get lastinsert ID failed, err:%v\n", err)
  12. return
  13. }
  14. fmt.Printf("insert success, the id is %d.\n", theID)
  15. }

更新数据

具体更新数据示例代码如下:

  1. // 更新数据
  2. func updateRowDemo() {
  3. sqlStr := "update user set age=? where id = ?"
  4. ret, err := db.Exec(sqlStr, 39, 3)
  5. if err != nil {
  6. fmt.Printf("update failed, err:%v\n", err)
  7. return
  8. }
  9. n, err := ret.RowsAffected() // 操作影响的行数
  10. if err != nil {
  11. fmt.Printf("get RowsAffected failed, err:%v\n", err)
  12. return
  13. }
  14. fmt.Printf("update success, affected rows:%d\n", n)
  15. }

删除数据

具体删除数据的示例代码如下:

  1. // 删除数据
  2. func deleteRowDemo() {
  3. sqlStr := "delete from user where id = ?"
  4. ret, err := db.Exec(sqlStr, 3)
  5. if err != nil {
  6. fmt.Printf("delete failed, err:%v\n", err)
  7. return
  8. }
  9. n, err := ret.RowsAffected() // 操作影响的行数
  10. if err != nil {
  11. fmt.Printf("get RowsAffected failed, err:%v\n", err)
  12. return
  13. }
  14. fmt.Printf("delete success, affected rows:%d\n", n)
  15. }

MySQL预处理

什么是预处理?

普通SQL语句执行过程:

  1. 客户端对SQL语句进行占位符替换得到完整的SQL语句。
  2. 客户端发送完整SQL语句到MySQL服务端
  3. MySQL服务端执行完整的SQL语句并将结果返回给客户端。

预处理执行过程:

  1. 把SQL语句分成两部分,命令部分与数据部分。
  2. 先把命令部分发送给MySQL服务端,MySQL服务端进行SQL预处理。
  3. 然后把数据部分发送给MySQL服务端,MySQL服务端对SQL语句进行占位符替换。
  4. MySQL服务端执行完整的SQL语句并将结果返回给客户端。

为什么要预处理?

  1. 优化MySQL服务器重复执行SQL的方法,可以提升服务器性能,提前让服务器编译,一次编译多次执行,节省后续编译的成本。
  2. 避免SQL注入问题。

Go实现MySQL预处理

database/sql中使用下面的Prepare方法来实现预处理操作。

  1. func (db *DB) Prepare(query string) (*Stmt, error)

Prepare方法会先将sql语句发送给MySQL服务端,返回一个准备好的状态用于之后的查询和命令。返回值可以同时执行多个查询和命令。

查询操作的预处理示例代码如下:

  1. // 预处理查询示例
  2. func prepareQueryDemo() {
  3. sqlStr := "select id, name, age from user where id > ?"
  4. stmt, err := db.Prepare(sqlStr)
  5. if err != nil {
  6. fmt.Printf("prepare failed, err:%v\n", err)
  7. return
  8. }
  9. defer stmt.Close()
  10. rows, err := stmt.Query(0)
  11. if err != nil {
  12. fmt.Printf("query failed, err:%v\n", err)
  13. return
  14. }
  15. defer rows.Close()
  16. // 循环读取结果集中的数据
  17. for rows.Next() {
  18. var u user
  19. err := rows.Scan(&u.id, &u.name, &u.age)
  20. if err != nil {
  21. fmt.Printf("scan failed, err:%v\n", err)
  22. return
  23. }
  24. fmt.Printf("id:%d name:%s age:%d\n", u.id, u.name, u.age)
  25. }
  26. }

插入、更新和删除操作的预处理十分类似,这里以插入操作的预处理为例:

  1. // 预处理插入示例
  2. func prepareInsertDemo() {
  3. sqlStr := "insert into user(name, age) values (?,?)"
  4. stmt, err := db.Prepare(sqlStr)
  5. if err != nil {
  6. fmt.Printf("prepare failed, err:%v\n", err)
  7. return
  8. }
  9. defer stmt.Close()
  10. _, err = stmt.Exec("小王子", 18)
  11. if err != nil {
  12. fmt.Printf("insert failed, err:%v\n", err)
  13. return
  14. }
  15. _, err = stmt.Exec("沙河娜扎", 18)
  16. if err != nil {
  17. fmt.Printf("insert failed, err:%v\n", err)
  18. return
  19. }
  20. fmt.Println("insert success.")
  21. }

SQL注入问题

我们任何时候都不应该自己拼接SQL语句!

这里我们演示一个自行拼接SQL语句的示例,编写一个根据name字段查询user表的函数如下:

  1. // sql注入示例
  2. func sqlInjectDemo(name string) {
  3. sqlStr := fmt.Sprintf("select id, name, age from user where name='%s'", name)
  4. fmt.Printf("SQL:%s\n", sqlStr)
  5. var u user
  6. err := db.QueryRow(sqlStr).Scan(&u.id, &u.name, &u.age)
  7. if err != nil {
  8. fmt.Printf("exec failed, err:%v\n", err)
  9. return
  10. }
  11. fmt.Printf("user:%#v\n", u)
  12. }

此时以下输入字符串都可以引发SQL注入问题:

  1. sqlInjectDemo("xxx' or 1=1#")
  2. sqlInjectDemo("xxx' union select * from user #")
  3. sqlInjectDemo("xxx' and (select count(*) from user) <10 #")

补充:不同的数据库中,SQL语句使用的占位符语法不尽相同。

数据库 占位符语法
MySQL ?
PostgreSQL $1, $2
SQLite ?$1
Oracle :name

Go实现MySQL事务

什么是事务?

事务:一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元),同时这个完整的业务需要执行多次的DML(insert、update、delete)语句共同联合完成。A转账给B,这里面就需要执行两次update操作。

在MySQL中只有使用了Innodb数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性,保证成批的SQL语句要么全部执行,要么全部不执行。

事务的ACID

通常事务必须满足4个条件(ACID):原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。

条件 解释
原子性 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
一致性 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
隔离性 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
持久性 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

事务相关方法

Go语言中使用以下三个方法实现MySQL中的事务操作。 开始事务

  1. func (db *DB) Begin() (*Tx, error)

提交事务

  1. func (tx *Tx) Commit() error

回滚事务

  1. func (tx *Tx) Rollback() error

事务示例

下面的代码演示了一个简单的事务操作,该事物操作能够确保两次更新操作要么同时成功要么同时失败,不会存在中间状态。

  1. // 事务操作示例
  2. func transactionDemo() {
  3. tx, err := db.Begin() // 开启事务
  4. if err != nil {
  5. if tx != nil {
  6. tx.Rollback() // 回滚
  7. }
  8. fmt.Printf("begin trans failed, err:%v\n", err)
  9. return
  10. }
  11. sqlStr1 := "Update user set age=30 where id=?"
  12. ret1, err := tx.Exec(sqlStr1, 2)
  13. if err != nil {
  14. tx.Rollback() // 回滚
  15. fmt.Printf("exec sql1 failed, err:%v\n", err)
  16. return
  17. }
  18. affRow1, err := ret1.RowsAffected()
  19. if err != nil {
  20. tx.Rollback() // 回滚
  21. fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
  22. return
  23. }
  24. sqlStr2 := "Update user set age=40 where id=?"
  25. ret2, err := tx.Exec(sqlStr2, 3)
  26. if err != nil {
  27. tx.Rollback() // 回滚
  28. fmt.Printf("exec sql2 failed, err:%v\n", err)
  29. return
  30. }
  31. affRow2, err := ret2.RowsAffected()
  32. if err != nil {
  33. tx.Rollback() // 回滚
  34. fmt.Printf("exec ret1.RowsAffected() failed, err:%v\n", err)
  35. return
  36. }
  37. fmt.Println(affRow1, affRow2)
  38. if affRow1 == 1 && affRow2 == 1 {
  39. fmt.Println("事务提交啦...")
  40. tx.Commit() // 提交事务
  41. } else {
  42. tx.Rollback()
  43. fmt.Println("事务回滚啦...")
  44. }
  45. fmt.Println("exec trans success!")
  46. }

sqlx库使用指南

在项目中我们通常可能会使用database/sql连接MySQL数据库。本文借助使用sqlx实现批量插入数据的例子,介绍了sqlx中可能被你忽视了的sqlx.InDB.NamedExec方法。

sqlx介绍

在项目中我们通常可能会使用database/sql连接MySQL数据库。sqlx可以认为是Go语言内置database/sql的超集,它在优秀的内置database/sql基础上提供了一组扩展。这些扩展中除了大家常用来查询的Get(dest interface{}, ...) errorSelect(dest interface{}, ...) error外还有很多其他强大的功能。

安装sqlx

  1. go get github.com/jmoiron/sqlx

基本使用

连接数据库

  1. var db *sqlx.DB
  2. func initDB() (err error) {
  3. dsn := "user:password@tcp(127.0.0.1:3306)/sql_test?charset=utf8mb4&parseTime=True"
  4. // 也可以使用MustConnect连接不成功就panic
  5. db, err = sqlx.Connect("mysql", dsn)
  6. if err != nil {
  7. fmt.Printf("connect DB failed, err:%v\n", err)
  8. return
  9. }
  10. db.SetMaxOpenConns(20)
  11. db.SetMaxIdleConns(10)
  12. return
  13. }

查询

查询单行数据示例代码如下:

  1. // 查询单条数据示例
  2. func queryRowDemo() {
  3. sqlStr := "select id, name, age from user where id=?"
  4. var u user
  5. err := db.Get(&u, sqlStr, 1)
  6. if err != nil {
  7. fmt.Printf("get failed, err:%v\n", err)
  8. return
  9. }
  10. fmt.Printf("id:%d name:%s age:%d\n", u.ID, u.Name, u.Age)
  11. }

查询多行数据示例代码如下:

  1. // 查询多条数据示例
  2. func queryMultiRowDemo() {
  3. sqlStr := "select id, name, age from user where id > ?"
  4. var users []user
  5. err := db.Select(&users, sqlStr, 0)
  6. if err != nil {
  7. fmt.Printf("query failed, err:%v\n", err)
  8. return
  9. }
  10. fmt.Printf("users:%#v\n", users)
  11. }

插入、更新和删除

sqlx中的exec方法与原生sql中的exec使用基本一致:

  1. // 插入数据
  2. func insertRowDemo() {
  3. sqlStr := "insert into user(name, age) values (?,?)"
  4. ret, err := db.Exec(sqlStr, "沙河小王子", 19)
  5. if err != nil {
  6. fmt.Printf("insert failed, err:%v\n", err)
  7. return
  8. }
  9. theID, err := ret.LastInsertId() // 新插入数据的id
  10. if err != nil {
  11. fmt.Printf("get lastinsert ID failed, err:%v\n", err)
  12. return
  13. }
  14. fmt.Printf("insert success, the id is %d.\n", theID)
  15. }
  16. // 更新数据
  17. func updateRowDemo() {
  18. sqlStr := "update user set age=? where id = ?"
  19. ret, err := db.Exec(sqlStr, 39, 6)
  20. if err != nil {
  21. fmt.Printf("update failed, err:%v\n", err)
  22. return
  23. }
  24. n, err := ret.RowsAffected() // 操作影响的行数
  25. if err != nil {
  26. fmt.Printf("get RowsAffected failed, err:%v\n", err)
  27. return
  28. }
  29. fmt.Printf("update success, affected rows:%d\n", n)
  30. }
  31. // 删除数据
  32. func deleteRowDemo() {
  33. sqlStr := "delete from user where id = ?"
  34. ret, err := db.Exec(sqlStr, 6)
  35. if err != nil {
  36. fmt.Printf("delete failed, err:%v\n", err)
  37. return
  38. }
  39. n, err := ret.RowsAffected() // 操作影响的行数
  40. if err != nil {
  41. fmt.Printf("get RowsAffected failed, err:%v\n", err)
  42. return
  43. }
  44. fmt.Printf("delete success, affected rows:%d\n", n)
  45. }

NamedExec

DB.NamedExec方法用来绑定SQL语句与结构体或map中的同名字段。

  1. func insertUserDemo()(err error){
  2. sqlStr := "INSERT INTO user (name,age) VALUES (:name,:age)"
  3. _, err = db.NamedExec(sqlStr,
  4. map[string]interface{}{
  5. "name": "七米",
  6. "age": 28,
  7. })
  8. return
  9. }

NamedQuery

DB.NamedExec同理,这里是支持查询。

  1. func namedQuery(){
  2. sqlStr := "SELECT * FROM user WHERE name=:name"
  3. // 使用map做命名查询
  4. rows, err := db.NamedQuery(sqlStr, map[string]interface{}{"name": "七米"})
  5. if err != nil {
  6. fmt.Printf("db.NamedQuery failed, err:%v\n", err)
  7. return
  8. }
  9. defer rows.Close()
  10. for rows.Next(){
  11. var u user
  12. err := rows.StructScan(&u)
  13. if err != nil {
  14. fmt.Printf("scan failed, err:%v\n", err)
  15. continue
  16. }
  17. fmt.Printf("user:%#v\n", u)
  18. }
  19. u := user{
  20. Name: "七米",
  21. }
  22. // 使用结构体命名查询,根据结构体字段的 db tag进行映射
  23. rows, err = db.NamedQuery(sqlStr, u)
  24. if err != nil {
  25. fmt.Printf("db.NamedQuery failed, err:%v\n", err)
  26. return
  27. }
  28. defer rows.Close()
  29. for rows.Next(){
  30. var u user
  31. err := rows.StructScan(&u)
  32. if err != nil {
  33. fmt.Printf("scan failed, err:%v\n", err)
  34. continue
  35. }
  36. fmt.Printf("user:%#v\n", u)
  37. }
  38. }

事务操作

对于事务操作,我们可以使用sqlx中提供的db.Beginx()tx.Exec()方法。示例代码如下:

  1. func transactionDemo2()(err error) {
  2. tx, err := db.Beginx() // 开启事务
  3. if err != nil {
  4. fmt.Printf("begin trans failed, err:%v\n", err)
  5. return err
  6. }
  7. defer func() {
  8. if p := recover(); p != nil {
  9. tx.Rollback()
  10. panic(p) // re-throw panic after Rollback
  11. } else if err != nil {
  12. fmt.Println("rollback")
  13. tx.Rollback() // err is non-nil; don't change it
  14. } else {
  15. err = tx.Commit() // err is nil; if Commit returns error update err
  16. fmt.Println("commit")
  17. }
  18. }()
  19. sqlStr1 := "Update user set age=20 where id=?"
  20. rs, err := tx.Exec(sqlStr1, 1)
  21. if err!= nil{
  22. return err
  23. }
  24. n, err := rs.RowsAffected()
  25. if err != nil {
  26. return err
  27. }
  28. if n != 1 {
  29. return errors.New("exec sqlStr1 failed")
  30. }
  31. sqlStr2 := "Update user set age=50 where i=?"
  32. rs, err = tx.Exec(sqlStr2, 5)
  33. if err!=nil{
  34. return err
  35. }
  36. n, err = rs.RowsAffected()
  37. if err != nil {
  38. return err
  39. }
  40. if n != 1 {
  41. return errors.New("exec sqlStr1 failed")
  42. }
  43. return err
  44. }

sqlx.In

sqlx.Insqlx提供的一个非常方便的函数。

sqlx.In的批量插入示例

表结构

为了方便演示插入数据操作,这里创建一个user表,表结构如下:

  1. CREATE TABLE `user` (
  2. `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  3. `name` VARCHAR(20) DEFAULT '',
  4. `age` INT(11) DEFAULT '0',
  5. PRIMARY KEY(`id`)
  6. )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

结构体

定义一个user结构体,字段通过tag与数据库中user表的列一致。

  1. type User struct {
  2. Name string `db:"name"`
  3. Age int `db:"age"`
  4. }

bindvars(绑定变量)

查询占位符?在内部称为bindvars(查询占位符),它非常重要。你应该始终使用它们向数据库发送值,因为它们可以防止SQL注入攻击。database/sql不尝试对查询文本进行任何验证;它与编码的参数一起按原样发送到服务器。除非驱动程序实现一个特殊的接口,否则在执行之前,查询是在服务器上准备的。因此bindvars是特定于数据库的:

  • MySQL中使用?
  • PostgreSQL使用枚举的$1$2等bindvar语法
  • SQLite中?$1的语法都支持
  • Oracle中使用:name的语法

bindvars的一个常见误解是,它们用来在sql语句中插入值。它们其实仅用于参数化,不允许更改SQL语句的结构。例如,使用bindvars尝试参数化列或表名将不起作用:

  1. // ?不能用来插入表名(做SQL语句中表名的占位符)
  2. db.Query("SELECT * FROM ?", "mytable")
  3. // ?也不能用来插入列名(做SQL语句中列名的占位符)
  4. db.Query("SELECT ?, ? FROM people", "name", "location")

自己拼接语句实现批量插入

比较笨,但是很好理解。就是有多少个User就拼接多少个(?, ?)

  1. // BatchInsertUsers 自行构造批量插入的语句
  2. func BatchInsertUsers(users []*User) error {
  3. // 存放 (?, ?) 的slice
  4. valueStrings := make([]string, 0, len(users))
  5. // 存放values的slice
  6. valueArgs := make([]interface{}, 0, len(users) * 2)
  7. // 遍历users准备相关数据
  8. for _, u := range users {
  9. // 此处占位符要与插入值的个数对应
  10. valueStrings = append(valueStrings, "(?, ?)")
  11. valueArgs = append(valueArgs, u.Name)
  12. valueArgs = append(valueArgs, u.Age)
  13. }
  14. // 自行拼接要执行的具体语句
  15. stmt := fmt.Sprintf("INSERT INTO user (name, age) VALUES %s",
  16. strings.Join(valueStrings, ","))
  17. _, err := DB.Exec(stmt, valueArgs...)
  18. return err
  19. }

使用sqlx.In实现批量插入

前提是需要我们的结构体实现driver.Valuer接口:

  1. func (u User) Value() (driver.Value, error) {
  2. return []interface{}{u.Name, u.Age}, nil
  3. }

使用sqlx.In实现批量插入代码如下:

  1. // BatchInsertUsers2 使用sqlx.In帮我们拼接语句和参数, 注意传入的参数是[]interface{}
  2. func BatchInsertUsers2(users []interface{}) error {
  3. query, args, _ := sqlx.In(
  4. "INSERT INTO user (name, age) VALUES (?), (?), (?)",
  5. users..., // 如果arg实现了 driver.Valuer, sqlx.In 会通过调用 Value()来展开它
  6. )
  7. fmt.Println(query) // 查看生成的querystring
  8. fmt.Println(args) // 查看生成的args
  9. _, err := DB.Exec(query, args...)
  10. return err
  11. }

使用NamedExec实现批量插入

注意 :该功能需1.3.1版本以上,并且1.3.1版本目前还有点问题,sql语句最后不能有空格和;,详见issues/690

使用NamedExec实现批量插入的代码如下:

  1. // BatchInsertUsers3 使用NamedExec实现批量插入
  2. func BatchInsertUsers3(users []*User) error {
  3. _, err := DB.NamedExec("INSERT INTO user (name, age) VALUES (:name, :age)", users)
  4. return err
  5. }

把上面三种方法综合起来试一下:

  1. func main() {
  2. err := initDB()
  3. if err != nil {
  4. panic(err)
  5. }
  6. defer DB.Close()
  7. u1 := User{Name: "七米", Age: 18}
  8. u2 := User{Name: "q1mi", Age: 28}
  9. u3 := User{Name: "小王子", Age: 38}
  10. // 方法1
  11. users := []*User{&u1, &u2, &u3}
  12. err = BatchInsertUsers(users)
  13. if err != nil {
  14. fmt.Printf("BatchInsertUsers failed, err:%v\n", err)
  15. }
  16. // 方法2
  17. users2 := []interface{}{u1, u2, u3}
  18. err = BatchInsertUsers2(users2)
  19. if err != nil {
  20. fmt.Printf("BatchInsertUsers2 failed, err:%v\n", err)
  21. }
  22. // 方法3
  23. users3 := []*User{&u1, &u2, &u3}
  24. err = BatchInsertUsers3(users3)
  25. if err != nil {
  26. fmt.Printf("BatchInsertUsers3 failed, err:%v\n", err)
  27. }
  28. }

sqlx.In的查询示例

关于sqlx.In这里再补充一个用法,在sqlx查询语句中实现In查询和FIND_IN_SET函数。即实现SELECT * FROM user WHERE id in (3, 2, 1);SELECT * FROM user WHERE id in (3, 2, 1) ORDER BY FIND_IN_SET(id, '3,2,1');

in查询

查询id在给定id集合中的数据。

  1. // QueryByIDs 根据给定ID查询
  2. func QueryByIDs(ids []int)(users []User, err error){
  3. // 动态填充id
  4. query, args, err := sqlx.In("SELECT name, age FROM user WHERE id IN (?)", ids)
  5. if err != nil {
  6. return
  7. }
  8. // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它
  9. query = DB.Rebind(query)
  10. err = DB.Select(&users, query, args...)
  11. return
  12. }

in查询和FIND_IN_SET函数

查询id在给定id集合的数据并维持给定id集合的顺序。

  1. // QueryAndOrderByIDs 按照指定id查询并维护顺序
  2. func QueryAndOrderByIDs(ids []int)(users []User, err error){
  3. // 动态填充id
  4. strIDs := make([]string, 0, len(ids))
  5. for _, id := range ids {
  6. strIDs = append(strIDs, fmt.Sprintf("%d", id))
  7. }
  8. query, args, err := sqlx.In("SELECT name, age FROM user WHERE id IN (?) ORDER BY FIND_IN_SET(id, ?)", ids, strings.Join(strIDs, ","))
  9. if err != nil {
  10. return
  11. }
  12. // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它
  13. query = DB.Rebind(query)
  14. err = DB.Select(&users, query, args...)
  15. return
  16. }

当然,在这个例子里面你也可以先使用IN查询,然后通过代码按给定的ids对查询结果进行排序。

参考链接:

Illustrated guide to SQLX

使用sqlx批量插入数据的若干方法

2020年3月30日

本文总阅读量2734次

在项目中我们通常可能会使用database/sql连接MySQL数据库。本文借助使用sqlx实现批量插入数据的例子,介绍了sqlx中可能被你忽视了的sqlx.InDB.NamedExec方法。

在项目中我们通常可能会使用database/sql连接MySQL数据库。sqlx可以认为是Go语言内置database/sql的超集,它在优秀的内置database/sql基础上提供了一组扩展。这些扩展中除了大家常用来查询的Get(dest interface{}, ...) errorSelect(dest interface{}, ...) error外还有很多其他强大的功能。

本文借助使用sqlx实现批量插入数据的例子,介绍了sqlx中可能被你忽视了的sqlx.InDB.NamedExec方法。

前置条件

连接数据库

  1. var DB *sqlx.DB
  2. func initDB() (err error) {
  3. dsn := "username:password@tcp(127.0.0.1:3306)/sqlx_demo"
  4. // 也可以使用MustConnect连接不成功就panic
  5. DB, err = sqlx.Connect("mysql", dsn)
  6. if err != nil {
  7. fmt.Printf("connect DB failed, err:%v\n", err)
  8. return
  9. }
  10. fmt.Println("connecting to MySQL...")
  11. return
  12. }

表结构

  1. CREATE TABLE `user` (
  2. `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  3. `name` VARCHAR(20) DEFAULT '',
  4. `age` INT(11) DEFAULT '0',
  5. PRIMARY KEY(`id`)
  6. )ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

结构体

  1. type User struct {
  2. Name string `db:"name"`
  3. Age int `db:"age"`
  4. }

bindvars(绑定变量)

查询占位符?在内部称为bindvars(查询占位符),它非常重要。你应该始终使用它们向数据库发送值,因为它们可以防止SQL注入攻击。database/sq不尝试对查询文本进行任何验证;它与编码的参数一起按原样发送到服务器。除非驱动程序实现一个特殊的接口,否则在执行之前,查询是在服务器上准备的。因此bindvars是特定于数据库的:

  • MySQL中使用?
  • PostgreSQL使用枚举的$1$2等bindvar语法
  • SQLite中?$1的语法都支持
  • Oracle中使用:name的语法

其他数据库可能有所不同。可以使用sqlx.DB.Rebind(string) string函数和?的bindvar语法去获取适合在当前数据库类型上执行的查询语句。

bindvars的一个常见误解是,它们用于在语句中插入值。它们仅用于参数化,不允许更改SQL语句的结构。例如,使用bindvars尝试参数化列或表名将不起作用:

  1. // ?不能用来插入表名(做SQL语句中表名的占位符)
  2. db.Query("SELECT * FROM ?", "mytable")
  3. // ?也不能用来插入列名(做SQL语句中列名的占位符)
  4. db.Query("SELECT ?, ? FROM people", "name", "location")

如何实现批量插入呢?

自己拼接语句实现批量插入

比较笨,但是很好理解。就是有多少个User就拼接多少个(?, ?)

  1. // BatchInsertUsers 自行构造批量插入的语句
  2. func BatchInsertUsers(users []*User) error {
  3. // 存放 (?, ?) 的slice
  4. valueStrings := make([]string, 0, len(users))
  5. // 存放values的slice
  6. valueArgs := make([]interface{}, 0, len(users) * 2)
  7. // 遍历users准备相关数据
  8. for _, u := range users {
  9. // 此处占位符要与插入值的个数对应
  10. valueStrings = append(valueStrings, "(?, ?)")
  11. valueArgs = append(valueArgs, u.Name)
  12. valueArgs = append(valueArgs, u.Age)
  13. }
  14. // 自行拼接要执行的具体语句
  15. stmt := fmt.Sprintf("INSERT INTO user (name, age) VALUES %s",
  16. strings.Join(valueStrings, ","))
  17. _, err := DB.Exec(stmt, valueArgs...)
  18. return err
  19. }

使用sqlx.In实现批量插入

前提是需要我们的结构体实现driver.Valuer接口:

  1. func (u User) Value() (driver.Value, error) {
  2. return []interface{}{u.Name, u.Age}, nil
  3. }

使用sqlx.In实现批量插入代码如下:

  1. // BatchInsertUsers2 使用sqlx.In帮我们拼接语句和参数, 注意传入的参数是[]interface{}
  2. func BatchInsertUsers2(users []interface{}) error {
  3. query, args, _ := sqlx.In(
  4. "INSERT INTO user (name, age) VALUES (?), (?), (?)",
  5. users..., // 如果arg实现了 driver.Valuer, sqlx.In 会通过调用 Value()来展开它
  6. )
  7. fmt.Println(query) // 查看生成的querystring
  8. fmt.Println(args) // 查看生成的args
  9. _, err := DB.Exec(query, args...)
  10. return err
  11. }

使用NamedExec实现批量插入

注意 :该功能目前有人已经推了#285 PR,但是作者还没有发release,所以想要使用下面的方法实现批量插入需要暂时使用master分支的代码:

在项目目录下执行以下命令下载并使用master分支代码:

  1. go get github.com/jmoiron/sqlx@master

使用NamedExec实现批量插入的代码如下:

  1. // BatchInsertUsers3 使用NamedExec实现批量插入
  2. func BatchInsertUsers3(users []*User) error {
  3. _, err := DB.NamedExec("INSERT INTO user (name, age) VALUES (:name, :age)", users)
  4. return err
  5. }

把上面三种方法综合起来试一下:

  1. func main() {
  2. err := initDB()
  3. if err != nil {
  4. panic(err)
  5. }
  6. defer DB.Close()
  7. u1 := User{Name: "七米", Age: 18}
  8. u2 := User{Name: "q1mi", Age: 28}
  9. u3 := User{Name: "小王子", Age: 38}
  10. // 方法1
  11. users := []*User{&u1, &u2, &u3}
  12. err = BatchInsertUsers(users)
  13. if err != nil {
  14. fmt.Printf("BatchInsertUsers failed, err:%v\n", err)
  15. }
  16. // 方法2
  17. users2 := []interface{}{u1, u2, u3}
  18. err = BatchInsertUsers2(users2)
  19. if err != nil {
  20. fmt.Printf("BatchInsertUsers2 failed, err:%v\n", err)
  21. }
  22. // 方法3
  23. users3 := []*User{&u1, &u2, &u3}
  24. err = BatchInsertUsers3(users3)
  25. if err != nil {
  26. fmt.Printf("BatchInsertUsers3 failed, err:%v\n", err)
  27. }
  28. }

sqlx.In的查询示例

关于sqlx.In这里再补充一个用法,在sqlx查询语句中实现In查询和FIND_IN_SET函数。即实现SELECT * FROM user WHERE id in (3, 2, 1);SELECT * FROM user WHERE id in (3, 2, 1) ORDER BY FIND_IN_SET(id, '3,2,1');

in查询

查询id在给定id集合中的数据。

  1. // QueryByIDs 根据给定ID查询
  2. func QueryByIDs(ids []int)(users []User, err error){
  3. // 动态填充id
  4. query, args, err := sqlx.In("SELECT name, age FROM user WHERE id IN (?)", ids)
  5. if err != nil {
  6. return
  7. }
  8. // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它
  9. query = DB.Rebind(query)
  10. err = DB.Select(&users, query, args...)
  11. return
  12. }

in查询和FIND_IN_SET函数

查询id在给定id集合的数据并维持给定id集合的顺序。

  1. // QueryAndOrderByIDs 按照指定id查询并维护顺序
  2. func QueryAndOrderByIDs(ids []int)(users []User, err error){
  3. // 动态填充id
  4. strIDs := make([]string, 0, len(ids))
  5. for _, id := range ids {
  6. strIDs = append(strIDs, fmt.Sprintf("%d", id))
  7. }
  8. query, args, err := sqlx.In("SELECT name, age FROM user WHERE id IN (?) ORDER BY FIND_IN_SET(id, ?)", ids, strings.Join(strIDs, ","))
  9. if err != nil {
  10. return
  11. }
  12. // sqlx.In 返回带 `?` bindvar的查询语句, 我们使用Rebind()重新绑定它
  13. query = DB.Rebind(query)
  14. err = DB.Select(&users, query, args...)
  15. return
  16. }

当然,在这个例子里面你也可以先使用IN查询,然后通过代码按给定的ids对查询结果进行排序。

参考链接:

Illustrated guide to SQLX

GO学习-(23) Go语言操作MySQL + 强大的sqlx的更多相关文章

  1. 使用Go语言操作MySQL数据库的思路与步骤

    最近在做注册登录服务时,学习用Go语言操作MySQL数据库实现用户数据的增删改查,现将个人学习心得总结如下,另外附有代码仓库地址,欢迎各位有兴趣的fork. 软件环境:Goland.Navicat f ...

  2. Linux C语言操作MySQL

    原文:Linux C语言操作MySQL 1.MySQL数据库简介 MySQL是一个开源码的小型关系数据库管理系统,体积小,速度快,总体成本低,开源.MySQL有以下特性: (1) 使用C和C++编写, ...

  3. Go语言操作MySQL数据库

    Go语言操作MySQL数据库 MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品.MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用 ...

  4. c语言操作mysql数据库

    c语言操作Mysql数据库,主要就是为了实现对数据库的增.删.改.查等操作,操作之前,得先连接数据库啊,而连接数据库主要有两种方法.一.使用mysql本身提供的API,在mysql的安装目录中可可以看 ...

  5. 用C语言操作MySQL数据库,进行连接、插入、修改、删除等操作

    C/C++ code   ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 3 ...

  6. GO学习-(24) Go语言操作Redis

    Go语言操作Redis 在项目开发中redis的使用也比较频繁,本文介绍了Go语言中go-redis库的基本使用. Redis介绍 Redis是一个开源的内存数据库,Redis提供了多种不同类型的数据 ...

  7. go语言操作mysql范例(增删查改)

    http://blog.csdn.net/jesseyoung/article/details/40398321 go语言连接mysql简介    go官方仅提供了database package,d ...

  8. Go语言操作MySQL

    MySQL是常用的关系型数据库,本文介绍了Go语言如何操作MySQL数据库. Go操作MySQL 连接 Go语言中的database/sql包提供了保证SQL或类SQL数据库的泛用接口,并不提供具体的 ...

  9. GO学习-(31) Go语言操作Elasticsearch

    Elasticsearch 本文简单介绍了ES.Kibana和Go语言操作ES. Elasticsearch 介绍 Elasticsearch(ES)是一个基于Lucene构建的开源.分布式.REST ...

随机推荐

  1. SpringBoot自动装配,比较全的吧,来看看吧~

    文章挺长,表达不好,希望能有获~~~~~~~ Spring也提供使用注解来注册bean,为什么要用SpringBoot呢? 使用Spring应用,比如SpringMVC还行需要配置ViewResolv ...

  2. 华中科大MOOC 操作系统原理讨论题

    1没有安装操作系统的计算机启动过程和结果? 启动会比较快,但功能很局限,无法使用常见的软件应用,对于普通用户来说,功能很局限,对于专业工程师来说,想使用没有操作系统的计算机也有难度.启动后进入 BIO ...

  3. SpringCloud(四)GateWay网关

    GateWay网关 概述简介 Gateway是在 Spring生态系统之上构建的AP网关服务,基于 Spring5, Spring Boot2和 Project Reactor等技术. Gateway ...

  4. vue 回顾

    ①.组件的注册 全局注册: // 注册 Vue.component('my-component', { template: '<div>A custom component!</di ...

  5. 【Idea】实用插件列表

    1.Translation : 翻译 2.CodeGlance:代码预览 3.Lombok:优雅编程插件 4.GenerateSerialVersionUID :自动生成序列化ID 5.CamelCa ...

  6. PHP正则匹配各种匹配方法

    平时做网站经常要用正则表达式,下面是一些讲解和例子,仅供大家参考和修改使用: 匹配数字 "^\d+$" //非负整数(正整数 + 0) "[1][1-9][0-9]$&q ...

  7. POJ2570 二进制,位运算,Floyd

    题意:       给你一个有向图,两点之间有多种连接方式,然后每次询问都问你点A,B之间有哪些方式可以到达,每个小字母是一个方式. 思路:       很巧妙的位运算和Floyd应用,借助Floyd ...

  8. 路由器逆向分析------MIPS系统网络的配置(QEMU)

    本文博客地址:http://blog.csdn.net/qq1084283172/article/details/69378333 MIPS系统网络的配置  使用QEMU 模拟正在运行的MIPS系统并 ...

  9. POJ1611基础带权并查集

    题意:       有一个人生病了,和他一个社团或者间接和他有联系的人都会生病,问一共有多少人生病了. 思路:       比较简单和基础的题,带权并查集中的一种,就是记录更新集合元素个数,这个题目我 ...

  10. Blazor实现未登录重定向到登录页的方法

    今天研究了一下blazor,发现他默认启动就是类似于后台管理系统的界面,看到这个页面我就想给他写个登录,有登录就涉及到未登录重定向的问题,但是我没有找到blazor全局路由的设置,知道的老哥可以告诉我 ...