本节核心内容
  • 介绍MySQL中的基于sql.DB的常用方法
  • 介绍MySQL预处理的优势和流程原理
  • 介绍基于预处理方式的CRUD代码如何编写
  • 介绍一些开发中的注意事项

本小节视频教程和代码:百度网盘

可先下载视频和源码到本地,边看视频边结合源码理解后续内容,边学边练。

基于sql.DB的常用方法介绍
DB.QueryDB.QueryRowDB.ExecDB.BeginDB.PrepareDB.Close
预编译语句(Prepared Statement)
DB.Prepare()Tx.Prepare()

预处理的优势:

  • 可以实现自定义参数的查询
  • 通常来说, 比手动拼接字符串 SQL 语句高效.
  • 可以防止SQL注入攻击

预处理的流程:

  1. 将sql分为2部分.命令部分和数据部分.
  2. 首先将命令部分发送给mysql服务器,mysql进行预处理.(如生成AST)
  3. 然后将数据部分发送给mysql服务器,mysql进行占位符替换.
  4. mysql服务器执行sql语句,把执行结果发送给客户端.

一般用Prepared Statements和Exec()完成INSERT, UPDATE, DELETE操作。

注意事项:使用预处理进行查询操作时,不仅在defer时需要关闭结果集,而且还要关闭命令句柄,否则同样会占用连接,导致阻塞.

建立数据库表

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(20) DEFAULT '',
  `age` int(11) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4
查询
package main

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
	"fmt"
)

type User struct {
	Id int
	Name sql.NullString
	Age sql.NullInt64
}


func PrepareQuery(db *sql.DB, id int) {
	stmt, err := db.Prepare("select id, name, age from user where id > ?")
	if err != nil {
		panic(err)
	}

	rows, err := stmt.Query(id)
	if err != nil {
		panic(err)
	}

	defer stmt.Close()
	defer rows.Close()

	for rows.Next(){
		var user User
		err := rows.Scan(&user.Id, &user.Name, &user.Age)
		if err != nil {
			panic(err)
		}
		fmt.Printf("User.Id: %v\nUser.Name: %s\nUser.Age: %d\n", user.Id,user.Name.String,user.Age.Int64)
	}
}

func main() {

	dns := "root:root@tcp(127.0.0.1:3306)/test"
	db, err := sql.Open("mysql", dns)
	if err != nil {
		panic(err)
	}

	defer db.Close()

	PrepareQuery(db, 0)

}

代码解析:

db.Prepare()db.Query()stmt.Close()rows.Close()rows.Scan()db.Close()
增、删、改

在学习完查询方法后,咱们来学习一下如何增删改,下面给大家分享了一个基于预编译方式的通用的CRUD代码,代码如下:

package model

import (
	"database/sql"
	"log"

	"mysql/config"

	_ "github.com/go-sql-driver/mysql" //这个引用是必不可少的,因为需要调用driver.go文件里的init方法来提供一个数据库驱动程序
	"github.com/spf13/viper"
	"fmt"
)

var DB *sql.DB     //全局变量,这样可以在别处调用

func Init() *sql.DB {

	var err error
	//加载配置文件
	config.Init()

	//这行代码的作用就是初始化一个sql.DB对象
	DB ,err = sql.Open("mysql", viper.GetString("mysql.source_name"))
	if nil != err {
		panic(err)
	}

	//设置最大超时时间
	DB.SetMaxIdleConns(viper.GetInt("mysql.max_idle_conns"))

	//建立链接
	err = DB.Ping()
	if nil != err{
		panic(err)
	}else{
		log.Println("Mysql Startup Normal!")
	}
	return DB
}


//	Insert 插入操作
func Insert(sql string,args... interface{}) {
	stmt, err := DB.Prepare(sql)
	defer stmt.Close()
	CheckErr(err, "SQL语句设置失败")
	result, err := stmt.Exec(args...)
	CheckErr(err, "参数添加失败")
	id, err := result.LastInsertId()
	CheckErr(err, "插入失败")
	fmt.Printf("插入成功,ID为%v\n",id)
}

//	Delete 删除操作
func Delete(sql string,args... interface{})  {
	stmt, err := DB.Prepare(sql)
	defer stmt.Close()
	CheckErr(err, "SQL语句设置失败")
	result, err := stmt.Exec(args...)
	CheckErr(err, "参数添加失败")
	num, err := result.RowsAffected()
	CheckErr(err,"删除失败")
	fmt.Printf("删除成功,删除行数为%d\n",num)
}

//	Update 修改操作
func Update(sql string,args... interface{})  {
	stmt, err := DB.Prepare(sql)
	defer stmt.Close()
	CheckErr(err, "SQL语句设置失败")
	result, err := stmt.Exec(args...)
	CheckErr(err, "参数添加失败")
	num, err := result.RowsAffected()
	CheckErr(err,"修改失败")
	fmt.Printf("修改成功,修改行数为%d\n",num)
}

// CheckErr 用来校验error对象是否为空
func CheckErr(err error,msg string)  {
	if nil != err {
		log.Panicln(msg,err)
	}
}

代码解析:

db.Prepare()stmt.Close()stmt.LastInsertId()stmt.Exec()result.RowsAffected()

示例代码:

package main

import (
	_ "github.com/go-sql-driver/mysql"
	"mysql/model"
)

func main() {
	DB := model.Init()
	model.Insert("insert into user(name,age) values(?,?) ","jeck",18)
	model.Insert("insert into user(name,age) values(?,?) ","Lucy",16)
	model.Insert("insert into user(name,age) values(?,?) ","David",22)
	model.Update("update user set age = ? where name = ?",20,"Lucy")
	model.Delete("delete from user where name = ? ", "David")
	DB.Close()
}

运行结果

2019/01/23 19:32:39 Mysql Startup Normal!
插入成功,ID为3
插入成功,ID为4
插入成功,ID为5
修改成功,修改行数为1
删除成功,删除行数为1

注意事项

在使用sql.DB对MySQL进行操作时,有一些注意事项是需要我们注意的,下面将列出那些开发中的一些建议和问题思路

  1. 建议将rows.Scan 参数的顺序和需要查询的结果的column一一对应.

rows.Scan 参数的顺序很重要, 需要和查询的结果的column对应. 例如 “SELECT * From user where age >=20 AND age < 30” 查询的行的 column 顺序是 “id, name, age” 和插入操作顺序相同, 因此 rows.Scan 也需要按照此顺序 rows.Scan(&id, &name, &age), 不然会造成数据读取的错位.

  1. 建议将待查询的数据类型定义为sql.Nullxxx类型

因为golang是强类型语言,所以查询数据时先定义数据类型,但是查询数据库中的数据存在三种可能:存在值,存在零值,未赋值NULL 三种状态, 因为可以将待查询的数据类型定义为sql.Nullxxx类型,可以通过判断Valid值来判断查询到的值是否为赋值状态还是未赋值NULL状态.

  1. 建议在每次db.Query操作后, 都调用rows.Close().

因为 db.Query() 会从数据库连接池中获取一个连接, 这个底层连接在结果集(rows)未关闭前会被标记为处于繁忙状态。当遍历读到最后一条记录时,会发生一个内部EOF错误,自动调用rows.Close(),但如果提前退出循环,rows不会关闭,连接不会回到连接池中,连接也不会关闭, 则此连接会一直被占用. 因此通常我们使用 defer rows.Close() 来确保数据库连接可以正确放回到连接池中; 不过阅读源码发现rows.Close()操作是幂等操作,即一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同, 所以即便对已关闭的rows再执行close()也没关系.

小节

本小节主要讲解了MySQL的常用方法,其中主要讲了预编译方式的CRUD,以及预编译方式的优势和原理,最后总结了一些注意事项。