摒弃世俗浮躁
追求技术精湛

Golang笔记(四):数据库操作(原生)

数据库驱动

Go 官方提供了 database/sql 包来给用户进行和数据库打交道的工作,database/sql 库实际只提供了一套操作数据库的接口和规范,例如抽象好的 SQL 预处理(prepare),连接池管理,数据绑定,事务,错误处理等。官方并没有提供具体某种数据库驱动。

Tips:维护标准接口的好处是 —— 将数据库操作抽象出来,相当于同一份代码可用于多种数据库中,只需要换个驱动而已。比较常见的场景 —— 生产环境使用 MySQL / PostgreSQL 等专业数据库,测试时可使用速度更快的内存 SQLite 数据库。

引入驱动

通过go mod下载安装该MySQL驱动,命令如下:

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

下载完成后要在项目中做匿名导入:

package main

import (
    _ "github.com/go-sql-driver/mysql"
    ...省略代码...
)

为什么需要匿名导入?

因为引入的是驱动,操作数据库时我们使用的是 sql 库里的方法,而不会具体使用到 go-sql-driver/mysql 包里的方法,当有未使用的包被引入时,Go 编译器会停止编译。为了让编译器能正常运行,需要使用 匿名导入 来加载。
当导入了一个数据库驱动后,此驱动会自行初始化(利用 init() 函数)并注册自己到 Golang 的 database/sql 上下文中。

Tips:Go 语言中,为了使用导入的程序包,必须首先对其进行初始化。初始化始终在单个线程中执行,并且以程序包依赖关系的顺序执行。初始化每个包后,会优先自动执行 init() 函数,并且执行优先级高于主函数的执行优先级。

连接数据库

// initDB 初始化数据库.
func initDB() {
	var err error
	config := mysql.Config{
		User:                 "root",
		Passwd:               "FLHY1VJ0WEOBIG3N",
		Addr:                 "127.0.0.1:3306",
		Net:                  "tcp",
		DBName:               "goblog",
		AllowNativePasswords: true,
	}

	// 设置数据库连接池
	db, err = sql.Open("mysql", config.FormatDSN())
	checkError(err)

	// 设置最大连接数
	db.SetMaxOpenConns(25)

	// 设置最大空闲连接数
	db.SetMaxIdleConns(25)

	// 设置每个链接的过期时间
	db.SetConnMaxLifetime(5 * time.Minute)

	err = db.Ping()
	checkError(err)
}

// checkError 检查数据库是否报错,如果有报错会将信息写进log.
func checkError(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

func main() {
	initDB()
        ...代码省略...
}
注意:上面代码当中,在 sql.Open() 的时候,并没有连接数据库,只是为连接数据库做好了准备。通常我们会再跟上一个 db.ping() 来检查连接的状态。

原生SQL

Exec

一般使用 sql.DB 中的 Exec() 来执行没有返回结果集的 SQL 语句(例如 INSERT, UPDATE, DELETE 等语句)。
语法如下:

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

参数为纯文本模式,不使用 Prepare,只发送一条 SQL 查询:

db.Exec("DELETE FROM articles WHERE id = " + strconv.FormatInt(a.ID, 10))

多参数为 Prepare 模式,底层使用 Prepare 语句,会发送两条 SQL 查询:

query := "UPDATE articles SET title = ?, body = ? WHERE id = ?"
rs, err := db.Exec(query, title, body, id)

返回的Result实现了 sql.Result 接口,代码如下:

type Result interface {
   // 使用 INSERT 向数据插入记录,数据表有自增 id 时,该函数有返回值
   LastInsertId() (int64, error)

   // 表示影响的数据表行数
   RowsAffected() (int64, error)
}

对于INSERT, UPDATE, DELETE操作我们可以使用LastInsertId()方法或者RowsAffected()方法来判断SQL语句是否执行成功,对于其他类型的SQL语句我们直接判断返回的error即可。

QueryRow

一般情况下,我们使用 QueryRow() 来读取单条的数据。这是个可变参数的方法,具体语法如下:

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

它的参数可以为一个或者多个。参数只有一个的情况下,我们称之为纯文本模式,多个参数的情况下称之为 Prepare 模式。之所以称之为 Prepare 模式是因为当多个参数的情况下,QueryRow() 封装了 Prepare 方法的调用,也就是说,下面这段代码:

err := db.QueryRow(query, id).Scan(&article.ID, &article.Title, &article.Body)

等同于:

stmt, err := db.Prepare(query)
checkError(err)
defer stmt.Close()
err = stmt.QueryRow(id).Scan(&article.ID, &article.Title, &article.Body)

使用 QueryRow() 的 Prepare 模式不仅保证了安全性,更能提升可读性。

注意:使用 Prepare 模式会发送两个 SQL 请求到 MySQL 服务器上,而纯文本模式只有一个。

Query

一般情况下,我们使用 Query() 从数据库中读取多条数据,而前面的 QueryRow() 是读取单条的数据。语法如下:

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

调用方式与 QueryRow() 和 Exec() 一致,支持单一参数的纯文本模式,以及多个参数的 Prepare 模式。
Query() 和 Exec() 的区别在于Exec 只会返回插入的 ID 和影响行数,而 Query 会返回数据表里的内容(结果集),使用场景不同而已。

Scan方法

QueryRow() 会返回一个 sql.Row 结构体,通常我们使用链式调用的方式调用 sql.Row.Scan() 方法:

db.QueryRow(query, id).Scan(&article.ID, &article.Title, &article.Body)

Scan() 将查询结果赋值到我们的 article struct 中,传参应与数据表字段的顺序保持一致。
需要注意的是,返回的 sql.Row 是个指针变量,保存有 SQL 连接。当调用 Scan() 时,就会将连接释放。所以在每次 QueryRow 后使用 Scan 是必须的。

sql.Rows 对象

sql.Rows包含的方法如下:

func (rs *Rows) Close() error                            //关闭结果集
func (rs *Rows) ColumnTypes() ([]*ColumnType, error)    //返回数据表的列类型
func (rs *Rows) Columns() ([]string, error)             //返回数据表列的名称
func (rs *Rows) Err() error                      // 错误集
func (rs *Rows) Next() bool                      // 游标,下一行
func (rs *Rows) Scan(dest ...interface{}) error  // 扫描结构体
func (rs *Rows) NextResultSet() bool

结果集在检出完 err 以后,遍历数据之前,应调用 defer rows.Close() 来关闭 SQL 连接。
一般我们会使用 rows.Next() 来遍历数据,如:

var articles []Article
//2. 循环读取结果
for rows.Next() {
    var article Article
    // 2.1 扫码每一行的结果并赋值到一个 article 对象中
    err := rows.Scan(&article.ID, &article.Title, &article.Body)
    checkError(err)
    // 2.2 将 article 追加到 articles 的这个数组中
    articles = append(articles, article)
}
// 2.3 检测循环时是否发生错误
err = rows.Err()
checkError(err)

循环完毕需检测是否发生错误。
rows.Scan() 参数的顺序很重要,需要和查询的结果的 column 对应。

sql.Row 和 sql.Rows对象

QueryRow() 返回 Row 对象,这是基于 Rows 实现的。尽管在 Row 结构体当中含有一个 Rows 对象,但它最多只能存在一条数据:

type Row struct {
	err  error
	rows *Rows
}

而 Query() 则返回 sql.Rows 结构体,代表一个查询结果集,这里面包含从数据库里读取出来的(多条)数据和 SQL 连接:

type Rows struct {
	dc          *driverConn
	releaseConn func(error)
	rowsi       driver.Rows
	cancel      func()
	closeStmt   *driverStmt

	closemu sync.RWMutex
	closed  bool
	lasterr error

	lastcols []driver.Value
}

Query() 与 sql.Rows 需要注意的点

参考代码 – 查询多条数据

  1. 在每一次 for rows.Next() 后,都记得要检测下是否有错误发生,调用 rows.Err() 可获取到错误;
  2. 使用 rows.Next() 遍历数据,遍历到最后内部遇到 EOF 错误,会自动调用 rows.Close() 将 SQL 连接关闭;
  3. 使用 rows.Next() 遍历时,如遇错误,SQL 连接也会自动关闭;
  4. rows.Close() 可调用多次,使用 rows.Close() 可保证 SQL 连接永远是关闭的。defer rows.Close() 需在检测 err 以后调用,否则会让运行时 panic ;
  5. 牢记在获取到结果集后,必须执行 defer rows.Close()。这样做能防止有时你在函数里过早 return ,或者其他操作忘记关闭资源,这是一个值得培养的良好习惯;
  6. 如果你在循环中执行 Query() 并获取 Rows 结果集,请不要使用 defer ,而是直接调用 rows.Close(),因为 defer 不会立刻执行,而是在函数执行结束后执行;

Prepare 和 Stmt

sql.DB.Prepare() 方法会返回一個 sql.Stmt 对象,与 Stmt 相关的方法如下:

stmt.Exec()
stmt.Query()
stmt.QueryRow()
stmt.Close()
注意与 sql.DB 下的方法区分。

做单独的语句查询时,谨记调用 defer stmt.Close() 来关闭 SQL 连接。
使用 Prepare 语句会发送两次请求到数据库服务器上,第一次是调用 Prepare() 语句时,第二次是调用以上提到的四个 Stmt 方法时:

Prepare 语句可有效防范 SQL 注入攻击。
以下常见的 DB 查询方法中,调用在传参一个以上时,底层皆会使用 Prepare 来发送请求:

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

sql.Tx 事务

使用以下可以开启事务:

func (db *DB) Begin() (*Tx, error)
func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)

Begin() 和 BeginTxt() 方法返回一个 sql.Tx 结构体,他支持以上提到的几种查询方法:

func (tx *Tx) Exec(query string, args ...interface{}) (Result, error)
func (tx *Tx) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error)
func (tx *Tx) Query(query string, args ...interface{}) (*Rows, error)
func (tx *Tx) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)
func (tx *Tx) QueryRow(query string, args ...interface{}) *Row
func (tx *Tx) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row

// 预编译 Prepare
func (tx *Tx) Stmt(stmt *Stmt) *Stmt
func (tx *Tx) StmtContext(ctx context.Context, stmt *Stmt) *Stmt
func (tx *Tx) Prepare(query string) (*Stmt, error)
func (tx *Tx) PrepareContext(ctx context.Context, query string) (*Stmt, error)

使用这同一个 sql.Tx 对数据库进行操作,就会在同一个事务中提交。
当使用 sql.Tx 的操作方式操作数据后,需要使用 sql.Tx 的 Commit() 方法提交事务,如果出错,则可以使用 sql.Tx 中的 Rollback() 方法回滚事务,保持数据的一致性,下面是这两个方法的定义:

func (tx *Tx) Commit() error
func (tx *Tx) Rollback() error

例如:

func (s Service) DoSomething() (err error) {
    // 1. 创建事务
    tx, err := s.db.Begin()
    if err != nil {
        return
    }
    // 2. 如果请求失败,就回滚所有 SQL 操作,否则提交
    //    defer 会在当前方法的最后执行
    defer func() {
        if err != nil {
            tx.Rollback()
            return err
        }
        err = tx.Commit()
    }()

    // 3. 执行各种请求
    if _, err = tx.Exec(...); err != nil {
        return err
    }
    if _, err = tx.Exec(...); err != nil {
        return err
    }
    // ...
    return nil
}

Demo


增加数据

先看代码:

// saveArticleToDB 向数据库中保存一篇文章,如果插入成功则返回一个主键ID.
func saveArticleToDB(title string, body string) (int64, error) {
	// 变量初始化
	var (
		id   int64
		err  error
		rs   sql.Result
		stmt *sql.Stmt
	)

	// 1.获取一个 Prepare 声明语句
	stmt, err = db.Prepare("INSERT INTO articles (title, body) VALUES (?, ?)")
	if err != nil {
		return 0, err
	}

	// 2.在此函数运行结束后关闭此语句,防止占用SQL连接
	defer stmt.Close()

	// 3.执行请求,传参进入绑定的内容
	rs, err = stmt.Exec(title, body)
	if err != nil {
		return 0, err
	}

	// 4.插入成功的话,会返回自增的ID
	if id, err = rs.LastInsertId(); id > 0 {
		return id, err
	}
	return 0, err
}

采用预编译的方式来规避SQL注入,代码详细解释如下:

  1. 先通过 db.Prepare() 来将SQL语句预编译,参数用 ? 进行占位
  2. 利用defer来延迟释放连接,避免长时间占用连接造成 ERROR 1040: Too many connections 错误
  3. Prepare 只会生产 stmt ,真正执行请求的需要调用 stmt.Exec(),而 stmt.Exec() 的参数依次对应 db.Prepare() 参数中 SQL 变量占位符 ?
  4. 由于 stmt.Exec() 返回值是一个 sql.Result 对象,所以在代码中直接调用 rs.LastInsertId() 来获取自增ID来判断是否增加数据成功

提示:返回的 LastInsertId 是int64位类型,可以通过 strconv.FormatInt(lastInsertID, 10) 来将其转换为字符串,其中第二个参数 10 代表十进制。

查询单条数据

// articlesShowHandler 文章详情.
func articlesShowHandler(w http.ResponseWriter, r *http.Request) {
	// 1.获取 URL 参数
	vars := mux.Vars(r)
	id := vars["id"]

	// 2.读取对应的文章数据
	article := Article{}
	query := "SELECT * FROM articles WHERE id = ?"
	err := db.QueryRow(query, id).Scan(&article.ID, &article.Title, &article.Body)

	// 3.如果出现错误
	if err != nil {
		// 判断是没找到数据 还是查询报错了
		if err == sql.ErrNoRows {
			w.WriteHeader(http.StatusNotFound)
			fmt.Fprint(w, "文章未找到")
		} else {
			checkError(err)
			w.WriteHeader(http.StatusInternalServerError)
			fmt.Fprint(w, "500 服务器内部错误")
		}
	} else {

		// 4.读取数据成功
		tmpl, err := template.ParseFiles("resources/views/articles/show.gohtml")
		checkError(err)
		tmpl.Execute(w, article)
	}
}

代码都很简单,可以让人一目了然,但需要多注意db.QueryRowScan位置的使用。

查询多条数据

// articlesIndexHandler 文章列表.
func articlesIndexHandler(w http.ResponseWriter, r *http.Request) {
	// 1.执行查询语句,返回一个结果集
	rows, err := db.Query("SELECT * FROM articles")
	checkError(err)
	defer rows.Close()

	// 2.循环读取结果
	var articles []Article
	for rows.Next() {
		var article Article
		// 2.1扫描每一行的结果并赋值到一个 article 对象中
		err := rows.Scan(&article.ID, &article.Title, &article.Body)
		checkError(err)
		// 2.2将 article 追加到 articles 这个切片当中
		articles = append(articles, article)
	}

	// 2.3检测遍历时是否发生错误
	err = rows.Err()
	checkError(err)

	// 3.加载模板
	tmpl, err := template.ParseFiles("resources/views/articles/index.gohtml")
	checkError(err)

	// 4.渲染模板,将所有文章的数据传输进去
	tmpl.Execute(w, articles)
}

释义参见如下:

赞(2) 打赏
未经允许不得转载:时光日记 » Golang笔记(四):数据库操作(原生)

评论 抢沙发

评论前必须登录!

 

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏