天天看点

go语言通过database/sql使用mysql数据库,以及以及第三方sqlx扩展go语言通过database/sql使用mysql数据库,以及以及第三方sqlx扩展

go语言通过database/sql使用mysql数据库,以及以及第三方sqlx扩展

mysql支持插件式的存储引擎, 常见的存储引擎有:MyISAM,InnoDB

  1. MyISAM:
    1. 查询速度快
    2. 只支持表锁
    3. 不支持事务
  2. InnoDB:
    1. 整体速度快
    2. 支持表锁和行锁
    3. 支持事务

事务:多个SQL操作为一个整体执行

事务特点:ACID

  1. 原子性
  2. 一致性
  3. 隔离性
  4. 持久性

索引:

原理:B树和B+树

需要掌握的知识点: 索引类型 命中 分库分表 SQL注入 SQL慢优化查询 MYSQL主从 读写分离

database/sql

原生支持连接池,是并发安全的

该标准库没有具体实现,只列出第三方库需要实现的具体内容

mysql驱动

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

           

连接数据库

package main

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

func main() {
	// 连接数据库
	dsn:="root:【填入自己的数据库密码】@tcp(127.0.0.1:3306)/hello"
	// 连接数据库
	db,err:=sql.Open("mysql",dsn)// 不会校验用户和密码石是否正确,只会校验格式是否正确
	if err != nil {               // dsn格式不正确会报错
		fmt.Printf(" %s invalid, err:%v\n",dsn,err)
		return
	}
	err=db.Ping()
	if err != nil {
		fmt.Printf("Open %s failed, err:%v\n",dsn,err)
		return
	}
    db.SetMaxOpenConns(10)// 最大连接数
	db.SetMaxIdleConns(3)// 最大空闲连接数
	fmt.Println("连接数据库成功!")
}
           

增删改查

package main

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

var db *sql.DB //一个连接池

type user struct{
	id int `db:"id"`
	name string `db:"name"`
	age int `db:"age"`
}
func initDB()(err error) {
	// 连接数据库
	dsn:="root:[email protected](127.0.0.1:3306)/hello"
	// 连接数据库
	db,err=sql.Open("mysql",dsn)// 不会校验用户和密码石是否正确,只会校验格式是否正确
	if err != nil {               // dsn格式不正确会报错
		//fmt.Printf(" %s invalid, err:%v\n",dsn,err)
		return 
	}
	err=db.Ping()
	if err != nil {
		//fmt.Printf("Open %s failed, err:%v\n",dsn,err)
		return 
	}
	// fmt.Println("连接数据库成功!")
	db.SetMaxOpenConns(10)// 最大连接数
	db.SetMaxIdleConns(3)// 最大空闲连接数
	return
}

func queryone(id int)(u *user){
	u=&user{}
	// 1. 查询单条记录的sql语句
	sqlstr:="select id,name,age from user where id=?;"
	// 2.执行
	db.QueryRow(sqlstr,id).Scan(&u.id,&u.name,&u.age) // 从连接池取取一个连接出来,去数据库查询单条记录,并调用scan拿结果
	// 3 返回结果
	return u
}

func queryMore(n int){
	// 1. sql语句
	sqlstr:="select id,name,age from user where id > ?"
	// 2 执行
	rows, err := db.Query(sqlstr, n)
	if err!=nil{
		fmt.Printf(" exec %s query faile, err := %v",sqlstr,err)
		return
	}
	// 3 关闭数据库连接
	defer rows.Close()
	// 循环取数
	for rows.Next(){
		var u1 user
		err:=rows.Scan(&u1.id,&u1.name,&u1.age)
		if err != nil{
			fmt.Printf("scan failed, err : %v\n",err)
		}
		fmt.Printf("user:%#v\n",u1)
	}
}

// 插入数据
func insert(name string,age int){
	// 1写sql语句
	sqlstr:="insert into user(name,age) values(?,?)"
	// 2 执行exec
	ret,err:=db.Exec(sqlstr,name,age)
	if err != nil {
		fmt.Printf("insert failed, err : %v\n",err)
		return
	}
	// 插入数据的操作,会拿到操作数据的id值
	id,err:=ret.LastInsertId()
	if err != nil {
		fmt.Printf("get id failed, err : %v\n",err)
		return
	}
	fmt.Println("id",id)
}

// 更新数据
func updateRow(newAge int,id int){
	sqlstr:="update user set age = ? where id =?"
	ret,err:=db.Exec(sqlstr,newAge,id)
	if err != nil {
		fmt.Printf("update failed, err : %v\n",err)
		return
	}
	n,err:=ret.RowsAffected()
	if err != nil {
		fmt.Printf("get id failed, err : %v\n",err)
		return
	}
	fmt.Printf("更新了%d行数据\n",n)
}

// 删除数据
func deleteRow(id int){
	sqlstr:="delete from user where id=?"
	ret,err := db.Exec(sqlstr,id)
	if err != nil {
		fmt.Printf("delete failed, err : %v\n",err)
		return
	}
	n,err:=ret.RowsAffected()
	if err != nil {
		fmt.Printf("get id failed, err : %v\n",err)
		return
	}
	fmt.Printf("删除了%d行数据\n",n)


}


func main() {
	err:=initDB()
	if err != nil {
		fmt.Printf("init DB failed, err : %v\n", err)
	}
	fmt.Println("连接数据库成功!")
	//insert("wangkai",400) 
	//queryone(2)
	//queryMore(3)
	 updateRow(34,2)
	//deleteRow(2)
}
           

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预处理:

Prepare

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

// 预处理方式插入多条数据
func prepareInsert(){
	sqlstr:="insert into user(name,age) values(?,?)"
	stmt,err := db.Prepare(sqlstr)
	if err != nil {
		fmt.Printf("prepar failed, err : %v\n",err)
		return
	}
	defer stmt.Close()
	// 后续只需要stmt 执行操作 
	var m= map[string]int{
		"dazhangwei":30,
		"lushuo":230,
		"zhangerwei":46,
	}
	for k,v := range m {
		stmt.Exec(k,v)
	}

}

           

Go语言实现事务

什么是事务?

事务:一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元),同时这个完整的业务需要执行多次的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中的事务操作。

开始事务

提交事务

回滚事务

func transaction(){
	// 开启事务
	tx,err := db.Begin()
	if err != nil {
		fmt.Printf("Transaction Begin failed, err: %v\n",err)
		return
	}
	// 执行多个sql操作
	sqlstr1 := "update user set age=age-2 where id=1"
	sqlstr2:="update user set age=age+2 where id=2"
	ret,err:=tx.Exec(sqlstr1)
	if err != nil{
		// 要回滚
		tx.Rollback()
		fmt.Println("执行sql1出错,要回滚")
	}
	if rows,_:=ret.RowsAffected();rows==0{
		tx.Rollback()
		fmt.Println("执行sql1出错,要回滚")
	}
	ret,err=tx.Exec(sqlstr2)
	if err != nil{
		// 要回滚
		tx.Rollback()
		fmt.Println("执行sql2出错,要回滚")
	}
	if rows,_:=ret.RowsAffected();rows==0{
		tx.Rollback()
		fmt.Println("执行sql2出错,要回滚")
	}
	 // 上面都执行成功,提交
	 err=tx.Commit()
	 if err != nil {
		 tx.Rollback()
		 fmt.Println("提交出错,要回滚")
	 }
	 fmt.Println("事务执行成功!")
}
           

sqlx 的使用

sqlx是一个go语言包,在内置database/sql包之上增加了很多扩展,简化数据库操作代码的书写

handle types

sqlx设计和database/sql使用方法是一样的。包含有4种主要的handle types:

  • sqlx.DB: 和sql.DB相似,表示数据库
  • sqlx.Tx: 和sql.Tx相似,表示transacion
  • sqlx.Stmt: 和sql.Stmt相似,表示prepared statement.
  • sqlx.NamedStmt: 表示prepared statement(支持named parameters)

所有的handler types都提供了对database/sql的兼容,意味着当用调用sqlx.DB.Query时,可以直接替换为sql.DB.Query。这就使得sqlx可以很容易的加入到已有的数据库项目中。

此外,sqlx还有两个cursor类型:

  • sqlx.Rows 和sql.Rows类似,Queryx返回。
  • sqlx.Row 和 sql.Row类似,QueryRowx返回。

与官方包sql的区别

1初始化:

sql:
func initDB() (err error) {
	// 连接数据库
	dsn := "root:[你的数据库密码]@tcp(127.0.0.1:3306)/hello"
	// 连接数据库
	db, err = sqlx.Open("mysql", dsn) // 不会校验用户和密码石是否正确,只会校验格式是否正确
	if err != nil {                  // dsn格式不正确会报错
		//fmt.Printf(" %s invalid, err:%v\n",dsn,err)
		return
	}
	err = db.Ping()
	if err != nil {
		//fmt.Printf("Open %s failed, err:%v\n",dsn,err)
		return
	}
	// fmt.Println("连接数据库成功!")
	db.SetMaxOpenConns(10) // 最大连接数
	db.SetMaxIdleConns(3)  // 最大空闲连接数
	return
}
           

sqlx:

func initDB() (err error) {
	// 连接数据库
	dsn := "root:[你的数据库密码]@tcp(127.0.0.1:3306)/hello"
	// 连接数据库
	
	db, err = sqlx.Connect("mysql",dsn) // 
	if err != nil {
		//fmt.Printf("connect failed, err : %v\n",err)
		return
	}
	// fmt.Println("连接数据库成功!")
	db.SetMaxOpenConns(10) // 最大连接数
	db.SetMaxIdleConns(3)  // 最大空闲连接数
	return
}
           

主要区别是connect 相当于sqlx的open和ping的结合,下面是connect的代码:

// Connect to a database and verify with a ping.
func Connect(driverName, dataSourceName string) (*DB, error) {
	db, err := Open(driverName, dataSourceName)
	if err != nil {
		return nil, err
	}
	err = db.Ping()
	if err != nil {
		db.Close()
		return nil, err
	}
	return db, nil
}

           

2 查询语句

sqlx中的handle types实现了数据库查询相同的基本的操作语法。

  • Exec(…) (sql.Result,error) 和database/sql相比没有改变
  • Query(…) (*sql.Rows, error) 和database/sql相比没有改变
  • QueryRow(…) *sql.Row 和database/sql相比没有改变

对内置语法的扩展

  • MustExec()sql.Result - Exec, but panic or error
  • Queryx(…) (*sqlx.Rows, error) - Query, but return an sqlx.Rows
  • QueryRows(…) *sqlx.Row - QueryRow, but return an sqlx.Row

扩展下面新的语法

  • Get(dest interface{},…) error
  • Select(dest interface{},…) error

因为官方的sql包query()和quertrow()在将查询到的数据传递到变量时,对于结构体变量,只能通过scan()方法逐一传递。sqlx包扩展了structscan()方法。可以对结果体变量直接传值。

例子:

type Place struct {
    Country       string
    City          sql.NullString
    TelephoneCode int `db:"telcode"`
}
// 多条查询
rows, err := db.Queryx("SELECT * FROM place")
for rows.Next() {
    var p Place
    err = rows.StructScan(&p)
}
// 单条查询
var p Place
err := db.QueryRowx("SELECT city, telcode FROM place LIMIT 1").StructScan(&p)
           
get 和select语法

他们是上述查询(query)和传值扫描(scan)操作的集合。

可扫描的定义:

  • a value is scannable if it is not a struct, eg

    string

    ,

    int

  • a value is scannable if it implements

    sql.Scanner

  • a value is scannable if it is a struct with no exported fields (eg.

    time.Time

    )

get和select使用

rows.Scan

方法扫描可扫描的数据,

rows.StructScan

扫描不可扫描的数据。get用于查询单条数据,select用于查询多条数据。

例子:

p := Place{}
pp := []Place{}
 
// this will pull the first place directly into p
err = db.Get(&p, "SELECT * FROM place LIMIT 1")
 
// this will pull places with telcode > 50 into the slice pp
err = db.Select(&pp, "SELECT * FROM place WHERE telcode > ?", 50)
 
// they work with regular types as well
var id int
err = db.Get(&id, "SELECT count(*) FROM place")
 
// fetch at most 10 place names
var names []string
err = db.Select(&names, "SELECT name FROM place LIMIT 10")
           

Get 和 Select 会关闭rows ,无需再调用rows.close()方法。

但是要注意的是,select会把查询结果一次性全放到内存,如果查询量较大的话,可以使用queryx和stuctscan结合的方法。

func Select(q Queryer, dest interface{}, query string, args ...interface{}) error {
	rows, err := q.Queryx(query, args...)
	if err != nil {
		return err
	}
	// if something happens here, we want to make sure the rows are Closed
	defer rows.Close()
	return scanAll(rows, dest, false)
}

           

3事务:

没啥区别

4 预处理:

Preparex和get方法可用于预处理

stmt, err := db.Preparex(`SELECT * FROM place WHERE telcode=?`)
var p Place
err = stmt.Get(&p, 852)
           

5 in 查询

因为database / sql不会检查您的查询,而是将您的参数直接传递给驱动程序,所以使用IN子句处理查询变得困难。

SELECT * FROM users WHERE level IN (?);
           

?只会接受一个单变量,但是更希望它接受一个可变数组。

var levels = []int{4, 6, 7}
rows, err := db.Query("SELECT * FROM users WHERE level IN (?);", levels)
           

可以先使用

sqlx.In

语法构建查询语句和参数列表。

var levels = []int{4, 6, 7}
query, args, err := sqlx.In("SELECT * FROM users WHERE level IN (?);", levels) 
// sqlx.In returns queries with the `?` bindvar, we can rebind it for our backend
query = db.Rebind(query)
rows, err := db.Query(query, args...)
           

例子:

sqlstr, args, err := sqlx.In("select id,category_name,category_no from category where id in (?)", categoryIds)
	if err != nil {
		return
	}
	// 查询
	err = DB.Select(&categoryList, sqlstr, args...)
           

##遇到的问题及解决方案

Q:go的time包的time.time类型与 mysql的date datetime 自动解析?

A:在dsn地址加上parseTime=true字段

Q:时差问题?

A:loc=local

package main

import (
    "database/sql"
    "fmt"
    "time"

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

func main() {
    db, err := sql.Open("mysql", 
    "root:6862273aliyun@tcp(39.97.229.151:3306)/weixiaobo?parseTime=true&loc=Local")

    var myTime time.Time
    rows, err := db.Query("SELECT current_timestamp()")
    fmt.Println(time.Now())
    if rows.Next() {
        if err = rows.Scan(&myTime); err != nil {
            panic(err)
        }
    }

    fmt.Println(myTime)
}