这是我参与「第三届青训营 -后端场」笔记创作活动的的第2篇笔记。
参考资料:
- database/sql 官方文档:go-database-sql.org/index.html
- GORM 官方文档:gorm.io/docs/
- 课程PPT:bytedance.feishu.cn/file/boxcnc…
database/sql
database 在应用中所处的位置:
在 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() 支持以 struct 或 map[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())找到。