Go 原生提供了连接数据库操作的支持,在用 Go 进行开发的时候,如果需要在和数据库交互,则可以使用 database/sql 包。这是一个对关系型数据库的通用抽象,它提供了标准的、轻量的、面向行的接口。
在 Go 中访问数据库需要用到sql.DB
接口:它可以创建语句(statement)和事务(transaction),执行查询、获取结果。
使用数据库时,除了database/sql
包本身,还需要引入想使用的特定数据库驱动。官方不提供实现,先下载第三方的实现,点击这里[1]查看各种各样的实现版本。
本文测试数据库为 MYSQL,使用的驱动为:github.com/go-sql-driver/mysql
,需要引入的包为:
"database/sql"
_ "github.com/go-sql-driver/mysql"
解释一下导入包名前面的"_"作用:
import 下划线(如:import * github/demo)的作用:当导入一个包时,该包下的文件里所有 init() 函数都会被执行,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行 init() 函数而已。这个时候就可以使用 import * 引用该包。
上面的 MYSQL 驱动中引入的就是 MYSQL 包中各个 init() 方法,你无法通过包名来调用包中的其他函数。导入时驱动的初始化函数会调用 sql.Register 将自己注册在 database/sql 包的全局变量 sql.drivers 中,以便以后通过 sql.Open 访问。
执行数据库操作之前我们准备一张表:
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT '',
`age` int(11) NOT NULL DEFAULT '0',
`sex` tinyint(3) NOT NULL DEFAULT '0',
`phone` varchar(45) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
1. 初始化数据库连接:
package init_config
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
var SqlDB *sql.DB
func init() {
SqlDB, _ = sql.Open("mysql", "root:123456789@tcp(127.0.0.1:3306)/test_db")
//设置数据库最大连接数
SqlDB.SetConnMaxLifetime(100)
//设置上数据库最大闲置连接数
SqlDB.SetMaxIdleConns(10)
//验证连接
if err := SqlDB.Ping(); err != nil {
fmt.Println("open database fail")
return
}
fmt.Println("connect success")
}
sql.Open() 中的数据库连接串格式为:
"用户名:密码@tcp(IP:端口)/数据库?charset=utf8"
。DB 的类型为:
*sql.DB
,有了 DB 之后我们就可以执行 CRUD 操作。Go 将数据库操作分为两类:Query
与Exec
。两者的区别在于前者会返回结果,而后者不会。
Query
表示查询,它会从数据库获取查询结果(一系列行,可能为空)。Exec
表示执行语句,它不会返回行。此外还有两种常见的数据库操作模式:
QueryRow
表示只返回一行的查询,作为Query
的一个常见特例。Prepare
表示准备一个需要多次使用的语句,供后续执行用。2. 查询操作
package dao
import (
"encoding/json"
"errors"
"fmt"
"mod-demo/init_config"
"strconv"
)
type User struct {
Id int64 `json:"id"`
Name string `json:"name"`
Age int32 `json:"age"`
Sex int8 `json:"sex"`
Phone string `json:"phone"`
}
//多行数据查询操作
func FindUserByIds(uids []int64) map[int64]User {
result := make(map[int64]User)
if len(uids) <= 0 {
return result
}
var searchParam string
for idx, uid := range uids {
if idx ==0 {
searchParam = strconv.FormatInt(uid, 10)
} else {
searchParam = fmt.Sprintf("%s,%v", searchParam, uid)
}
}
sql := "select * from user where id in (" + searchParam + ")"
rows, e := init_config.SqlDB.Query(sql)
if e == nil {
errors.New("query incur error")
}
for rows.Next() {
var user User
e := rows.Scan(&user.Id, &user.Name, &user.Age, &user.Sex, &user.Phone)
if e == nil {
bytes, _ := json.Marshal(user)
fmt.Println(string(bytes))
result[user.Id] = user
}
}
defer rows.Close()
return result
}
//单行查询操作
func FindUserById(uid int64) User {
var user User
searchSql := fmt.Sprintf("%s%v","select * from user where id=", uid)
init_config.SqlDB.QueryRow(searchSql).Scan(&user.Id, &user.Name, &user.Age, &user.Sex, &user.Phone)
return user
}
整体工作流程如下:
使用 db.Query()
来发送查询到数据库,获取结果集Rows
,并检查错误使用 rows.Next()
作为循环条件,迭代读取结果集使用 rows.Scan
从结果集中获取一行结果使用 rows.Err()
在退出迭代后检查错误使用 rows.Close()
关闭结果集,释放连接3. 查询预处理
普通 SQL 语句执行过程:
客户端对 SQL 语句进行占位符替换得到完整的 SQL 语句 客户端发送完整 SQL 语句到 MySQL 服务端 MySQL 服务端执行完整的 SQL 语句并将结果返回给客户端。 预处理执行过程:
把 SQL 语句分成两部分,命令部分与数据部分 先把命令部分发送给 MySQL 服务端,MySQL 服务端进行 SQL 预处理 然后把数据部分发送给 MySQL 服务端,MySQL 服务端对 SQL 语句进行占位符替换 MySQL 服务端执行完整的 SQL 语句并将结果返回给客户端。 预处理的好处有哪些呢?
优化 MySQ 服务器重复执行 SQL 的方法,可以提升服务器性能。提前让服务器编译,一次编译多次执行,节省后续编译的成本。 避免 SQL 注入问题。 使用 MySQL 预处理功能可以用 Prepare 方法:
func (db *DB) Prepare(query string) (*Stmt, error)
下面的示例演示了预处理 api 的使用:
//预处理使用
func PrepareUse(uid int64) User {
var user User
searchSql := fmt.Sprintf("%s%v", "select * from user where id=", uid)
searchSql = "select * from user where id = ?"
stmt, e := init_config.SqlDB.Prepare(searchSql)
if e != nil {
fmt.Println(e)
return User{}
}
defer stmt.Close()
rows, e := stmt.Query(uid)
if e != nil {
fmt.Println(e)
}
rows.Scan(&user.Id, &user.Name, &user.Age, &user.Sex, &user.Phone)
return user
}
同样预处理 api 也可以用于增删改的操作,这里就不多做演示。
4. 增删改和 Exec
通常不会约束你查询必须用 Query,只是 Query 会返回结果集,而 Exec 不会返回。所以如果你执行的是增删改操作一般用 Exec 会好一些。Exec 返回的结果是
Result
,Result
接口允许获取执行结果的元数据:type Result interface {
// 用于返回自增ID,并不是所有的关系型数据库都有这个功能。
LastInsertId() (int64, error)
// 返回受影响的行数。
RowsAffected() (int64, error)
}
在说 Exec 函数使用之前先了解一下占位符的概念。如果你现在想使用占位符的功能,where 的条件想以参数的形式传入,Go 提供了
db.Prepare
语句来帮你绑定。准备查询的结果是一个准备好的语句(prepared statement),语句中可以包含执行时所需参数的占位符(即绑定值)。准备查询比拼字符串的方式好很多,它可以转义参数避免 SQL 注入。同时,准备查询对于一些数据库也省去了解析和生成执行计划的开销,有利于性能。占位符
PostgreSQ L 使用
$N
作为占位符,N
是一个从 1 开始递增的整数,代表参数的位置,方便参数的重复使用。MySQL 使用?
作为占位符,SQLite 两种占位符都可以,而 Oracle 则使用:param1
的形式。MySQL PostgreSQL Oracle
===== ========== ======
WHERE col = ? WHERE col = $1 WHERE col = :col
VALUES(?, ?, ?) VALUES($1, $2, $3) VALUES(:val1, :val2, :val3)stmt, e := DB.Prepare("select * from user where id=?")
query, e := stmt.Query(1)
query.Scan()
接下来看一下插入操作的示例:
//插入新数据
func Insert(user User) (err error) {
sqlStr := "insert into user(name,age,sex,phone) values(?,?,?,?)"
result, err := init_config.SqlDB.Exec(sqlStr, user.Name, user.Age, user.Sex, user.Phone)
if err != nil {
return err
}
id, _ := result.LastInsertId()
fmt.Println("id is ", id)
return nil
}
更新操作:
//更新数据
func UpdateById(user User) (err error) {
sqlStr := "update user set name=?,age=?,sex=?,phone=? where id=?"
result, err := init_config.SqlDB.Exec(sqlStr, user.Name, user.Age, user.Sex, user.Phone, user.Id)
if err != nil {
return err
}
affectRow, _ := result.RowsAffected()
fmt.Println("affectRow is ", affectRow)
return nil
}
删除数据:
//删除数据
func DeleteById(id int64) (err error) {
sqlStr := "delete from user where id=?"
result, err := init_config.SqlDB.Exec(sqlStr, id)
if err != nil {
return err
}
affectRow, _ := result.RowsAffected()
fmt.Println("affectRow is ", affectRow)
return nil
}
5. 事务的使用
通过
db.Begin()
来开启一个事务,Begin
方法会返回一个事务对象Tx
。在结果变量Tx
上调用Commit()
或者Rollback()
方法会提交或回滚变更,并关闭事务。在底层,Tx
会从连接池中获得一个连接并在事务过程中保持对它的独占。事务对象Tx
上的方法与数据库对象sql.DB
的方法一一对应,例如Query,Exec
等。事务对象也可以准备(prepare)查询,由事务创建的准备语句会显式绑定到创建它的事务。func TestTx() {
//开启事务
tx, err := init_config.SqlDB.Begin()
if err != nil {
fmt.Println("tx fail")
}
//准备sql语句
stmt, err := tx.Prepare("DELETE FROM user WHERE id = ?")
if err != nil {
fmt.Println("Prepare fail")
return
}
//设置参数以及执行sql语句
res, err := stmt.Exec(2)
fmt.Println(res)
if err != nil {
fmt.Println("DELETE Exec fail")
return
}
panic(errors.New(" special err in the cur throw").Error())
sqlStr := "insert into user(name,age,sex,phone) values(?,?,?,?)"
result, err := tx.Exec(sqlStr, "方德峰",23, 1, "13321123543")
if err != nil {
fmt.Println("insert Exec fail")
return
}
affectRow, _ := result.RowsAffected()
fmt.Println("affectRow is ", affectRow)
//提交事务
tx.Commit()
}
上面的示例中,我在中间增加了一个 panic,它会阻止当前事务的成功提交,可以看到抛出这个异常之后前面的 delete 语句也被回滚了。大家可以试试去掉 panic 之后事务是否会成功提交。
本篇到这里就结束了,主要讲述 Go 原生的方式如何连接数据库。从上面的示例代码可以看到,如果你使用过 Java 的 Mybatis 或者 JPA 这些 ORM 框架的话,再让你去一句一句的拼装 SQL 你一定是痛苦的。当然在 Go 的世界中并没有像 Mybatis 这样成熟的 ORM 工具,但是也有一些目前为止比较优秀的比如:gorm,下一篇我们一起看看 gorm 的使用。
参考资料
[1] 点击这里: https://github.com/golang/go/wiki/SQLDrivers
文章评论