操作数据库
使用 Go 操作 MySQL 等数据库,一般有两种方式:
- database/sql 接口,直接在代码里硬编码 sql 语句;
- GORM,以对象关系映射的方式在抽象地操作数据库
database/sql 接口
安装
go get github.com/go-sql-driver/mysql
知识点:Go 语言中,为了使用导入的程序包,必须首先对其进行初始化。初始化始终在单个线程中执行,并且以程序包依赖关系的顺序执行。初始化每个包后,会优先自动执行 init() 函数,并且执行优先级高于主函数的执行优先级
连接数据库
package main
import (
"database/sql"
"fmt"
"log"
"net/http"
"net/url"
"strings"
"time"
"github.com/go-sql-driver/mysql"
"github.com/gorilla/mux"
// 匿名导入,因为引入的是驱动,操作数据库时我们使用的是 sql 库里的方法,而不会具体使用到 go-sql-driver/mysql 包里的方法,当有未使用的包被引入时,Go 编译器会停止编译。为了让编译器能正常运行,需要使用 匿名导入 来加载
_ "github.com/go-sql-driver/mysql"
)
var db *sql.DB
func main() {
// 初始化数据库
initDB()
}
func initDB() {
var err error
config := mysql.Config{
User: "root",
Passwd: "000000",
Addr: "127.0.0.1:3306",
Net: "tcp",
DBName: "go-blog",
AllowNativePasswords: true,
}
// 数据源信息,用于定义如何连接数据库
dsn := config.FormatDSN()
// 准备连接池
db, err = sql.Open("mysql", dsn)
checkError(err)
// 设置连接池最大打开数据库连接数,<= 0 表示无限制,默认为 0
db.SetMaxOpenConns(25)
// 设置连接池最大空闲数据库连接数,<= 0 表示不设置空闲连接数,默认为 2
db.SetMaxIdleConns(25)
// 设置每个链接的过期时间
db.SetConnMaxLifetime(5 * time.Minute)
// 尝试连接,失败会报错
err = db.Ping()
checkError(err)
}
创建表
func main() {
// 初始化数据库
initDB()
// 创建数据表
createTable()
}
func createTable() {
sql := `CREATE TABLE IF NOT EXISTS articles(
id bigint(20) PRIMARY KEY AUTO_INCREMENT NOT NULL,
title varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
body longtext COLLATE utf8mb4_unicode_ci
); `
// 处理没有返回结果集的 SQL 语句,insert update delete
_, err := db.Exec(sql)
checkError(err)
}
db.Exec
- 单参数(即只传query和一个参数)为纯文本模式,不使用 Prepare,只发送一条 SQL 查询
// 返回 Result 和 error
func (db *DB) Exec(query string, args ...interface{}) (Result, error) {
// context.Background() 是默认的上下文,这是一个空的 context ,我们无法对其进行取消、赋值、设置 deadline 等操作
return db.ExecContext(context.Background(), query, args...)
}
// 可以通过Result拿到id和受影响行数
type Result interface {
LastInsertId() (int64, error)
RowsAffected() (int64, error)
}
插入表
func main() {
// 初始化数据库
initDB()
// 创建数据表
createTable()
// 插入数据
id,err := saveArticleToDB("标题", "内容")
...
}
func saveArticleToDB(title string, body string) (int64, error) {
// 变量初始化
var (
id int64
err error
res 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.执行请求
res, err = stmt.Exec(title, body)
if err != nil {
return 0, err
}
// 4.插入成功
if id, err = res.LastInsertId(); id > 0 {
return id, nil
}
return 0, err
}
查询单条数据
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, "404")
} else {
// 数据库错误
checkError(err)
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprint(w, "服务器出错")
}
} else {
fmt.Fprint(w, article.Title)
}
db.QueryRow
func (db *DB) QueryRow(query string, args ...interface{}) *Row {
return db.QueryRowContext(context.Background(), query, args...)
}
// 扫描结构体
func (r *Row) Scan(dest ...interface{}) error {
if r.err != nil {
return r.err
}
defer r.rows.Close()
for _, dp := range dest {
if _, ok := dp.(*RawBytes); ok {
return errors.New("sql: RawBytes isn't allowed on Row.Scan")
}
}
if !r.rows.Next() {
if err := r.rows.Err(); err != nil {
return err
}
return ErrNoRows
}
err := r.rows.Scan(dest...)
if err != nil {
return err
}
// Make sure the query can be processed to completion with no errors.
return r.rows.Close()
}
查询结果集
// 执行查询语句,返回一个结果集
rows, err := db.Query("SELECT * FROM articles")
checkError(err)
// rows 没有自动关闭
defer rows.Close()
var articles []Article
// 读取
// 游标,下一行
for rows.Next() {
var article Article
// 扫描每一行的结果并赋值到一个对象中
err := rows.Scan(&article.ID, &article.Title, &article.Body)
checkError(err)
// 加入数组
articles = append(articles, article)
}
// 检测遍历时是否出错
// 错误集
err = rows.Err()
checkError(err)
Query
读取多条数据,QueryRow()
读取单条数据Query
支持单一参数的纯文本模式,以及多个参数的 Prepare 模式
db.Query
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
删除
rs, err := db.Exec("DELETE FROM articles WHERE id = " + strconv.FormatInt(a.ID, 10))
if err != nil {
return 0, err
}
if n, _ := rs.RowsAffected(); n > 0 {
return n, nil
}
事务
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
}
- 开启事务
- 返回一个
sql.Tx
结构体
func (db *DB) Begin() (*Tx, error)
func (db *DB) BeginTx(ctx context.Context, opts *TxOptions) (*Tx, error)
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)
// 提交和回滚
func (tx *Tx) Commit() error
func (tx *Tx) Rollback() error
Prepare 和 Stmt
sql.DB.Prepare()
方法会返回一個sql.Stmt
对象db.Exec
db.Query
db.QueryRow
,调用在传参一个以上时,底层皆会使用 Prepare 来发送请求
stmt.Exec()
stmt.Query()
stmt.QueryRow()
stmt.Close()
sql.Row
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
Context 上下文
- 所有的请求方法底层都是用其上下文版本的方法调用,且传入默认的上下文
func (db *DB) Exec(query string, args ...interface{}) (Result, error)
func (db *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (Result, error)
func (db *DB) Query(query string, args ...interface{}) (*Rows, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)
func (db *DB) QueryRow(query string, args ...interface{}) *Row
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *Row
题外话:新手入门,多有不解,理解有偏差请不吝指教
GORM
安装
$ go get -u gorm.io/gorm
// 在安装 GORM 的同时,我们还需要安装 GORM 的 MySQL 的数据库驱动
$ go get -u gorm.io/driver/mysql
通过id获取文章
func Get(idStr string) (Article, error) {
var article Article
id := types.StringToUint64(idStr)
// First() 是 gorm.DB 提供的用以从结果集中获取第一条数据的查询方法,第二个参数可以传参整型或者字符串 ID,但是传字符串会有 SQL 注入的风险
// .Error 是 GORM 的错误处理机制
// 找不到记录时,GORM 会返回 ErrRecordNotFound 错误
if err := model.DB.First(&article, id).Error; err != nil {
return article, err
}
return article, nil
}
获取所有文章
func GetAll() ([]Article, error) {
var articles []Article
if err := model.DB.Find(&articles).Error; err != nil {
return articles, err
}
return articles, nil
}
保存文章到数据库
func (article *Article) Create() (err error) {
if err := model.DB.Create(&article).Error; err != nil {
logger.LogError(err)
return err
}
return nil
}
更新文章
func (article *Article) Update() (rowsAffected int64, err error) {
result := model.DB.Save(&article)
if err := result.Error; err != nil {
logger.LogError(err)
return 0, err
}
return result.RowsAffected, nil
}
删除文章
func (article *Article) Delete() (rowsAffected int64, err error) {
result := model.DB.Delete(&article)
if err = result.Error; err != nil {
logger.LogError(err)
return 0, err
}
return result.RowsAffected, nil
}
疑问
在使用的过程中对以下两种写法有些混淆,加*
和不加的用法感觉很类似,这两种有什么区别呢?
func (article *Article) Delete(){}
func (article Article) Delete(){}