51·数据库进阶

database/sql 基础

sqldatabasequeryexec

第 51 课 - database/sql 基础

学习目标

通过本节课的学习,你将能够:

  1. 理解 Go 标准库 database/sql 的作用和基本概念。
  2. 掌握使用 database/sql 包连接数据库、执行查询和写入操作。
  3. 学会正确处理数据库操作中的错误和资源管理。
  4. 了解预编译语句(Prepare)的使用及其优势。
  5. 初步了解数据库连接池的基本配置。

核心概念

database/sql 是 Go 语言标准库中用于与关系型数据库交互的核心包。它提供了一套统一的接口,你可以通过导入不同的数据库驱动(如 github.com/go-sql-driver/mysqlgithub.com/mattn/go-sqlite3)来连接 MySQL、PostgreSQL、SQLite 等具体数据库。

关键概念:

  • 驱动(Driver):具体的数据库实现,通常是一个第三方包。
  • 数据源名称(DSN):连接数据库的字符串,格式因驱动而异(例如 "user:password@tcp(127.0.0.1:3306)/dbname")。
  • 连接池database/sql 会自动管理一组可复用的数据库连接,避免了频繁地打开和关闭连接。
  • 核心方法
    • db.Query():执行查询并返回多行结果。
    • db.QueryRow():执行查询并预期返回最多一行结果。
    • db.Exec():执行不返回行的语句(如 INSERT, UPDATE, DELETE)。
    • db.Prepare():创建预编译语句,提高重复执行的效率并防止SQL注入。

代码示例

下面是一个使用 database/sql 连接 SQLite 数据库并进行基本操作的完整示例。请确保已安装 go-sqlite3 驱动。

package main

import (
	"database/sql"
	"fmt"
	"log"

	// 导入 SQLite 驱动。它在导入时通过 init() 函数自动注册。
	// 下划线 `_` 表示我们只需要它的副作用(注册驱动),而不会直接使用包中的函数。
	_ "github.com/mattn/go-sqlite3"
)

// User 结构体用于映射数据库中的 user 表行
type User struct {
	ID    int
	Name  string
	Email string
}

func main() {
	// 1. 打开数据库连接。
	// DSN 对于 SQLite 是文件名。`./test.db` 会在当前目录创建或打开一个名为 test.db 的数据库文件。
	db, err := sql.Open("sqlite3", "./test.db")
	if err != nil {
		log.Fatalf("无法打开数据库: %v", err)
	}
	// 确保在 main 函数结束时关闭数据库连接池
	defer db.Close()

	// 2. 测试数据库连接是否真的可用
	err = db.Ping()
	if err != nil {
		log.Fatalf("无法连接到数据库: %v", err)
	}
	fmt.Println("成功连接到数据库!")

	// 3. 创建表(如果不存在)
	createTableSQL := `CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		name TEXT NOT NULL,
		email TEXT UNIQUE NOT NULL
	);`
	_, err = db.Exec(createTableSQL)
	if err != nil {
		log.Fatalf("创建表失败: %v", err)
	}
	fmt.Println("表 `users` 已就绪。")

	// 4. 插入数据
	insertSQL := `INSERT INTO users (name, email) VALUES (?, ?)`
	res, err := db.Exec(insertSQL, "张三", "[email protected]")
	if err != nil {
		log.Fatalf("插入数据失败: %v", err)
	}
	id, _ := res.LastInsertId()
	rowsAffected, _ := res.RowsAffected()
	fmt.Printf("插入成功。ID: %d, 影响行数: %d\n", id, rowsAffected)

	// 5. 查询单行数据
	var user User
	queryRowSQL := `SELECT id, name, email FROM users WHERE id = ?`
	err = db.QueryRow(queryRowSQL, 1).Scan(&user.ID, &user.Name, &user.Email)
	if err != nil {
		// 特别注意:QueryRow 在找不到记录时返回 sql.ErrNoRows 错误
		if err == sql.ErrNoRows {
			fmt.Println("未找到ID为1的用户")
		} else {
			log.Fatalf("查询单行数据失败: %v", err)
		}
	} else {
		fmt.Printf("查询到用户: %+v\n", user)
	}

	// 6. 查询多行数据
	querySQL := `SELECT id, name, email FROM users`
	rows, err := db.Query(querySQL)
	if err != nil {
		log.Fatalf("查询多行数据失败: %v", err)
	}
	// 关键:必须在使用完 rows 后关闭它,以释放连接回连接池
	defer rows.Close()

	var users []User
	for rows.Next() {
		var u User
		// 将当前行的数据扫描到结构体字段中
		err := rows.Scan(&u.ID, &u.Name, &u.Email)
		if err != nil {
			log.Fatalf("扫描行数据失败: %v", err)
		}
		users = append(users, u)
	}
	// 检查遍历过程中是否出错
	if err = rows.Err(); err != nil {
		log.Fatalf("遍历结果集时出错: %v", err)
	}
	fmt.Printf("查询到所有用户: %v\n", users)

	// 7. 使用预编译语句(Prepare)进行批量插入
	insertStmt, err := db.Prepare(insertSQL)
	if err != nil {
		log.Fatalf("预编译语句失败: %v", err)
	}
	defer insertStmt.Close()

	newUsers := []struct {
		Name  string
		Email string
	}{
		{"李四", "[email protected]"},
		{"王五", "[email protected]"},
	}
	for _, u := range newUsers {
		_, err := insertStmt.Exec(u.Name, u.Email)
		if err != nil {
			log.Printf("批量插入用户 %s 失败: %v\n", u.Name, err)
			continue
		}
		fmt.Printf("成功插入用户: %s\n", u.Name)
	}
}

如何运行:

  1. 初始化一个 Go 模块:go mod init example.com/sql-demo
  2. 获取依赖:go get github.com/mattn/go-sqlite3
  3. 运行程序:go run main.go

实践练习

练习 1:基础查询 编写一个程序,连接到 SQLite 数据库 school.db,创建一个 students 表(包含 id, name, grade 字段),插入至少3条记录,然后查询并打印出所有 grade 大于 90 的学生信息。

  • 预期输出示例:
    优秀学生:
    - 姓名: 小明, 年级: 95
    - 姓名: 小红, 年级: 92
    

练习 2:事务操作 在练习1的程序中添加一个函数 transferStudent(oldName, newGrade string)。该函数使用事务 (db.Begin()) 将一个学生的名字更新为 “已毕业” 并将其成绩设为 0。如果更新过程中发生任何错误,整个事务应回滚。

  • 要求:编写代码并测试在事务提交前模拟错误(如 rollback())和正常提交两种情况。

练习 3:错误处理与 NULL值 在数据库中添加一个 student_email 字段(允许为空 NULL)。编写代码查询一个存在但 email 为 NULL 的学生,并使用 sql.NullString 类型正确扫描和打印出结果。

  • 预期输出示例(当email为NULL时):
    学生: 小华, 邮箱: <未设置>
    

常见错误

  1. 忘记检查错误:几乎每个 database/sql 的方法都返回错误。忽略这些错误是程序不稳定和难以调试的主要原因。
  2. 资源泄露:忘记 defer rows.Close()defer db.Close()。这会导致数据库连接无法归还到连接池,最终耗尽连接资源。
  3. SQL注入:直接使用 fmt.Sprintf 拼接 SQL 字符串(如 fmt.Sprintf("... WHERE name = '%s'", userInput))是极其危险的。务必使用参数化查询(? 占位符)
  4. 误解 QueryRow 的错误:当 QueryRow 查不到记录时,它返回一个 sql.ErrNoRows 错误,而不是 nil。必须显式检查这个特定错误。
  5. 忽略 rows.Err():在循环 rows.Next() 结束后,应调用 rows.Err() 来检查迭代过程中是否发生错误(如连接中断)。
  6. NULL 值处理:对于数据库中允许 NULL 的字段,不能直接扫描到普通 Go 类型(如 string),而应使用 sql.NullString, sql.NullInt64 等类型。

小结

本节课我们学习了 Go 语言操作关系型数据库的基础——database/sql 包:

  • 它是标准接口:我们只需关心接口,通过驱动连接不同数据库。
  • 连接管理:使用 sql.Open 并传入正确的驱动名和 DSN 来建立连接。db.Ping() 用于验证连接。
  • 核心操作:掌握了 Exec(执行)、Query(查询多行)、QueryRow(查询单行)、Prepare(预编译)四个关键方法。
  • 错误处理:理解了 sql.ErrNoRows 等特定错误,并强调了持续检查错误的重要性。
  • 资源管理:必须使用 defer 关闭 dbrowsStmt 对象,这是防止资源泄露的关键。
  • 安全性:始终使用参数化查询(?)来防止 SQL 注入。

掌握了这些基础,你就可以为后续更复杂的数据库操作(如连接池配置、ORM 框架使用、复杂事务处理)打下坚实的基础。下一课,我们将聚焦于如何使用这些知识来操作 PostgreSQL 数据库。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「PostgreSQL 操作」 以巩固所学知识。