database/sql 与 Gorm 入门 | 青训营笔记

157 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。

参考资料:

database/sql

database 在应用中所处的位置:

image.png

在 Go 语言中,访问数据库是使用 sql.DB 类型,使用这个类型可以创建 statements 和 transactions,可以执行查询获取结果。

sql.DB 不是数据库连接,他也不是类似于数据库软件中数据库(database) 或 模式 (schema)。他是数据库接口和存在的抽象。

导入数据库驱动

这里以导入 mysql 数据库的驱动为例

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

虽然我们导入了 mysql 数据库的驱动,但我们不是直接使用它,而只是向 mysql 注册了这个驱动。使用 database/sql 对数据库进行操作是与数据库无关的,这样可以是我们的代码不依赖于特定的数据库。

访问数据库

在导入了数据库驱动包后,就可以创建 sql.DB 对象了

sql.Open() 返回 *sql.DB:

func main() {
	db, err := sql.Open("mysql",
		"user:password@tcp(127.0.0.1:3306)/hello")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()
}

sql.Open 的参数含义

  • 第一个参数是驱动的名称
  • 第二个参数是与数据库驱动相关的,对于 mysql 来说是 DSN(github.com/go-sql-driv…)

注意事项:

  • 要检查和处理 database/sql 中操作返回的错误
  • 在创建 sql.DB 后要关闭,在创建后使用 defer db.Close() 是一个惯用方法

反直觉的地方:sql.Open() 并不会立即与数据库建立连接

sql.Open() 并没有向数据库建立连接,也没有检查传入的连接参数是否是正确的。

只有第一次要使用数据库的时候,才会进行上述的操作。

如果我们想进行连接测试,可以使用 db.Ping()

err = db.Ping()
if err != nil {
	// do something here
}

如果连接失败,则会返回错误

对数据库进行查询

多行结果查询

多行结果查询使用 Query 函数,他会返回表中行的集合,即使结果集是空的,可以使用 Next() 对结果集进行遍历, 使用 Scan() 对结果集进行读取。

var (
	id int
	name string
)
rows, err := db.Query("select id, name from users where id = ?", 1)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
	err := rows.Scan(&id, &name)
	if err != nil {
		log.Fatal(err)
	}
	log.Println(id, name)
}
err = rows.Err()
if err != nil {
	log.Fatal(err)
}
  • 使用 Query() 函数返回的并不是结果集的一个数组,因为如果结果集很大的话,会占用大量的内存,所以返回的只是一个游标,一次只访问结果集中的一行数据。

  • defer rows.Close() 非常重要,因为如果不对结果集进行关闭,则会一直占用连接,导致资源的浪费,也影响了后面操作的进行。

  • Next() 函数用来准备结果集中的下一行(访问第一行也要调用),如果成功则返回 true,如果没有下一个结果集中的行了会自动调用 rows.Close() ,并返回 false,如果发生了错误也会返回 false,此时需要手动调用 rows.Close()。所以最好使用 defer rows.Close() 来确保 rows.Close() 的调用

  • rows.Scan() 用来读取一个 row 中的数据,Scan() 会自动进行数据类型的转换。

单行结果查询

使用 QueryRow() 来进行单行结果的查询

var name string
err = db.QueryRow("select name from users where id = ?", 1).Scan(&name)
if err != nil {
	log.Fatal(err)
}
fmt.Println(name)

Preparing Queries

使用 prepared statement 可以使一个查询语句被多次复用。在查询语句中可以用占位符表示参数,对比直接拼接字符串,这样做可以防止 SQL 注入。

在 MySQL 中参数占位符是 ? (PostgreSQL 是 $N

stmt, err := db.Prepare("select id, name from users where id = ?")
if err != nil {
	log.Fatal(err)
}
defer stmt.Close()
rows, err := stmt.Query(1)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
	// ...
}
if err = rows.Err(); err != nil {
	log.Fatal(err)
}

通过 Prepare() 函数可以返回一个 *Stmt

db.Query() 也进行了 prepares, executes, 和 closes a prepared statement。提前进行 prepare 的好处是提升了代码的复用性。

修改数据

修改数据使用 Exec() 函数

stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
	log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
	log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
	log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
	log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)

执行这个 statement 后,会返回一个 sql.Result

type Result interface {
	LastInsertId() (int64, error)
	RowsAffected() (int64, error)
}

可以通过 sql.Result 获取最后插入的 ID 和守影响的行数。

对于 Exec() 而言,返回的结果是可以被省略的,而Query() 则不行

_, err := db.Exec("DELETE FROM users")  // OK
_, err := db.Query("DELETE FROM users") // BAD

还有更多关于事务和连接池的相关操作可以查看官方的文档

Gorm 入门

下面记录了一下 Gorm 常用的增删改查操作,代码大部分来自于 Gorm 官方文档。

Create

用来增加数据,如果使用 struct 类型来插入数据,默认的 0 值

user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}

result := db.Create(&user) // pass pointer of data to Create

user.ID             // returns inserted data's primary key
result.Error        // returns error
result.RowsAffected // returns inserted records count

Query

First(), Take(), Last() 用来从数据库中检索单个对象。

First()Take() 的区别是 First 会对主键进行排序

Last() 返回最后一个,也会对主键进行排序

// Get the first record ordered by primary key
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1;

// Get one record, no specified order
db.Take(&user)
// SELECT * FROM users LIMIT 1;

// Get last record, ordered by primary key desc
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;

result := db.First(&user)
result.RowsAffected // returns count of records found
result.Error        // returns error or nil

// check error ErrRecordNotFound
errors.Is(result.Error, gorm.ErrRecordNotFound)

Query(), Take(), Last() 在查询的时候会加上 LIMIT 1 条件(比如 MySQL 中,如果没有找到记录的时候,它会返回 ErrRecordNotFound 错误,所以在使用上述函数的时候最好要对 error 的类型进行检查

errors.Is(result.Error, gorm.ErrRecordNotFound)

Update

更新所有字段使用 Save()

db.First(&user)

user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;

更新单个字段使用 Update()

// Update with conditions
db.Model(&User{}).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE active=true;

// User's ID is `111`:
db.Model(&user).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111;

// Update with conditions and model value
db.Model(&user).Where("active = ?", true).Update("name", "hello")
// UPDATE users SET name='hello', updated_at='2013-11-17 21:34:10' WHERE id=111 AND active=true;

更新多个字段使用 Updates()

Updates() 支持以 structmap[string]interface{} 进行更新,当使用 struct 进行更新时,只会更新非0值字段。

// Update attributes with `struct`, will only update non-zero fields
db.Model(&user).Updates(User{Name: "hello", Age: 18, Active: false})
// UPDATE users SET name='hello', age=18, updated_at = '2013-11-17 21:34:10' WHERE id = 111;

// Update attributes with `map`
db.Model(&user).Updates(map[string]interface{}{"name": "hello", "age": 18, "active": false})
// UPDATE users SET name='hello', age=18, active=false, updated_at='2013-11-17 21:34:10' WHERE id=111;

注意:当使用 struct 进行更新的时候,Gorm 只会更新非0值字段,如果想要确保指定字段被更新,应该使用 Select 更新选定字段,或使用 map 来完成更新操作。

Delete

删除一条记录

// Email's ID is `10`
db.Delete(&email)
// DELETE from emails where id = 10;

// Delete with additional conditions
db.Where("name = ?", "jinzhu").Delete(&email)
// DELETE from emails where id = 10 AND name = "jinzhu";

使用主键删除记录(可以删除多条记录)

db.Delete(&User{}, 10)
// DELETE FROM users WHERE id = 10;

db.Delete(&User{}, "10")
// DELETE FROM users WHERE id = 10;

db.Delete(&users, []int{1,2,3})
// DELETE FROM users WHERE id IN (1,2,3);

软删除 (Soft Delete)

如果模型包含 gorm.DeletedAt 字段(包含在 gorm.Model 中),会自动获得软删除的能力。

所谓的软删除,就是当调用 Delete 的时候,记录不会从数据库中溢出,但是 GRM 会设置 DeletedAt 的值为当前时间,然后 data 的值不会被普通的查询方法(Query())找到。