目录

Golang数据库操作

golang连接数据库需要使用到系统标准库 database/sql 和 数据库驱动,database/sql 支持数据库连接池,是并发安全的。

下面以mysql 为例,展示下连接,查询,写入,更新,删除,事务等操作。

下载数据库连接驱动

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

数据库操作

连接数据库

连接数据库使用 sql.Open() 函数打开数据库连接(此时只会判断DSN是否错误,不会连接数据库),并使用 db.Ping() 进行数据库连接。

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

dataSourceName 的格式为 username:password@tcp(host:port)/database

package main

import (
	"database/sql"
	"fmt"
	_ "github.com/go-sql-driver/mysql" // 导入mysql包,并不使用
)

// 连接mysql示例
func main() {
	dsn := "root:123123@tcp(127.0.0.1:3306)/golang"
	db, err := sql.Open("mysql", dsn) // 打开一个数据库连接,不会校验用户名和密码是否正确
	if err != nil {
		fmt.Printf("DSN[%s]格式错误: %v \n", dsn, err) // 如果出现错误,则DSN格式错误
		return
	}

	err = db.Ping() // 尝试连接mysql
 
	if err != nil {
		fmt.Printf("连接数据库失败: %v \n", err)
		return
	}

	fmt.Println("连接数库成功")
}

查询

单行查询

单行查询使用 db.QueryRow() 进行查询

func (db *DB) QueryRow(query string, args ...interface{}) *Row
func (r *Row) Scan(dest ...interface{}) error

QueryRow 函数会返回一个 *Row 指针,通过该指针对象的 Scan() 方法可以获得结果,如果没有查询到结果,会返回一个 sql.ErrNoRows

package main

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

var db *sql.DB

// 声明一个user结构体
type user struct {
	id       int
	nickname string
	avatar   *string // 数据库字段可以为空时,使用指针类型
}

func initDB() (err error) {
	dsn := "root:@tcp(127.0.0.1:3306)/golang"
	db, err = sql.Open("mysql", dsn) // 打开一个数据库连接,不会校验用户名和密码是否正确
	if err != nil {
		return err
	}
	err = db.Ping()

	if err != nil {
		return err
	}
	return
}

// 连接mysql示例
func main() {
	err := initDB() // 连接数据库

	if err != nil {
		log.Println("数据库连接失败,", err)
		return
	}
	sqlStr := "select id, nickname, avatar from users where id = ?" // 构建查询语句
	var u user
	err = db.QueryRow(sqlStr, 1).Scan(&u.id, &u.nickname, &u.avatar) // 查询单条数据并扫描结果

	if err != nil {
		if err == sql.ErrNoRows {
			log.Print("未查询到结果!")
      return
		}
		log.Printf("查询失败: %s", err)
    return
	}

	log.Printf("查询结果:%#v", u)
}

注意

  1. Scan 方法会释放当前的数据库连接回到连接池中,如果调用了QueryRow不调用Scan方法,则会导致数据库连接无法释放;
  2. Scan的时候,需要保证查询的字段顺序及数量和变量保持一致,否则会返回错误。
  3. 当数据库中字段可以为空时,字段应使用指针类型或sql.NullString
  4. 如果未查询到结果,Scan 会返回一个sql.ErrNoRows ,需要通过 err == sql.ErrNoRows 来确定。

多行查询

多行查询使用 db.Query() 函数进行查询,返回一个*Rowserror ;

示例:

// 多行查询
func queryMany() {
	sqlStr := "select id, nickname, avatar from users"

	rows, err := db.Query(sqlStr) // 查询sql语句
	if err != nil {
		fmt.Println("查询失败", err)
		return
	}
	users := make([]user, 0)

	defer rows.Close() // rows需要手动调用Close()释放连接
	for rows.Next() {  // 遍历调用Scan方法
		var u user
		err = rows.Scan(&u.id, &u.nickname, &u.avatar) // 对对象进行赋值
		if err != nil {
			fmt.Println("获取数据失败", err)
			continue
		}
		users = append(users, u)
	}
	fmt.Printf("%#v", users)
}

注意

  1. Rows 需要手动调用 Close() 方法释放数据库连接;
  2. 获取Rows 中的数据,需要使用循环来进行操作。

插入、修改和删除

插入、修改和删除统一通过 Exec 函数进行操作。

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

Exec 方法返回一个 Result 对象,该对象有两个方法:

LastInsertId() 用于获取插入的id,

RowsAffected() 用户获取受影响的行数。

插入数据

func insert() {
	sqlStr := "insert into users(name, age) values('王五', 10)" // 插入sql
	ret, err := db.Exec(sqlStr) // 执行插入语句

	if err != nil {
		fmt.Println("插入数据失败,", err)
		return
	}

	lastId, err := ret.LastInsertId() // 获取插入ID

	if err != nil {
		fmt.Println("插入数据失败,", err)
		return
	}

	fmt.Println("插入数据成功:", lastId)
}

更新数据

func update() {
	sqlStr := "update users set age = 12 where name = '王五'"

	ret, err := db.Exec(sqlStr)
	if err != nil {
		fmt.Println("更新数据失败", err)
		return
	}

	rows, err := ret.RowsAffected()

	if err != nil {
		fmt.Println("获取行数失败", err)
		return
	}

	fmt.Println("更新数据行数:", rows)
}

删除数据

func deleteRow() {
	sqlStr := "delete from users where age = 12"

	ret, err := db.Exec(sqlStr)

	if err != nil {
		fmt.Println("删除数据失败", err)
		return
	}

	rows, err := ret.RowsAffected()
	if err != nil {
		fmt.Println("获取行数失败", err)
		return
	}

	fmt.Println("删除行数:", rows)
}

Mysql 预处理

什么是预处理

普通Sql语句执行过程:

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

预处理sql语句执行过程

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

为什么要预处理?

  1. 优化mysql服务器重复执行sql的方法,提升服务器性能。对于相同的sql,可以一次编译多次执行,节省编译的性能和时间开销
  2. 避免sql注入

使用预处理

使用预处理需要使用到 db.Prepare() 方法

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

该方法返回一个 Stmt 的指针对象,通过调用StmtQuery(), QueryRow(), Exec() 等方法来执行sql语句,该对象需要调用 Close() 进行关闭

  1. 调用 db.Prepare() 方法进行sql预处理,得到Stmt 对象
  2. 通过调用StmtQuery(), QueryRow(), Exec() 等方法来执行sql语句,
  3. 关闭Stmt 对象
func prepareQueryDemo(id int) {
	sqlStr := "select id, name, age from users where id = ?"
	stmt, err := db.Prepare(sqlStr)

	if err != nil {
		fmt.Println("预处理失败", err)
		return
	}

	defer stmt.Close()
	row := stmt.QueryRow(id)

	var u user
	err = row.Scan(&u.id, &u.name, &u.age)

	if err != nil {
		fmt.Println("获取数据失败", err)
		return
	}

	fmt.Printf("获取数据成功%#v\n", u)

}

事务

事务使用 db.BeginTx 进行处理,开启事务后,事务内的sql需要使用返回的*Tx 进行处理。不能使用 db 进行操作,否则事务不生效。还可以通过sql.TxOption 配置隔离等级。

func transaction() {
	ctx := context.Background()
	tx, err := db.BeginTx(ctx, &sql.TxOptions{}) //开启事务
	if err != nil {
		log.Fatal("begin tx error:", err)
	}
  // do something
	sqlStr1 := "insert into users(nickname, avatar) values('王五', null)" // 插入sql
	_, err = tx.Exec(sqlStr1)                                           // 执行插入语句
	sqlStr2 := "insert into users(nickname, avatar) values('李四', null)" // 插入sql
	_, err = tx.Exec(sqlStr2)                                           // 执行插入语句
	if err != nil {
		tx.Rollback() // 事务回滚
		log.Println("Rollback", err)
		return
	}

	tx.Commit() // 事务提交
	log.Println("committed")
}