53·数据库进阶

MySQL 操作

mysqlgo-sql-driverquery

第53课:MySQL 操作

学习目标

完成本课学习后,你将能够:

  1. 使用Go的database/sql接口和go-sql-driver连接并操作MySQL数据库。
  2. 掌握使用Go执行MySQL的CRUD(创建、读取、更新、删除)基本操作。
  3. 理解并正确使用预编译语句(Prepared Statements)来安全高效地执行SQL。
  4. 学习如何处理数据库操作中的错误和资源管理(连接、行、语句)。
  5. 了解如何在Go中使用MySQL事务来保证一组操作的原子性。

核心概念

1. 数据库驱动与接口

Go语言通过标准库database/sql提供了统一的数据库操作接口,但本身不包含任何具体的数据库驱动。我们需要第三方驱动来连接MySQL。最常用的是github.com/go-sql-driver/mysql

你可以把 database/sql 想象成一个通用的遥控器,它定义了“换台”、“调音量”等标准按钮。而 go-sql-driver/mysql 就是适配某个特定品牌电视(MySQL)信号转换器。遥控器(接口)告诉你怎么操作,转换器(驱动)确保信号能被电视(MySQL数据库)理解。

2. 连接池管理

database/sql会自动维护一个数据库连接池。你不需要为每个请求都创建新连接,只需打开一个*sql.DB对象,它内部会智能地管理和复用连接,这极大地提高了性能。你需要配置连接池的最大打开连接数和最大空闲连接数。

3. 主要操作

  • 连接: sql.Open() 返回一个 *sql.DB 对象,它代表一个连接池,而不是一个连接。
  • 查询: Query() 用于查询多行数据,QueryRow() 用于查询单行。
  • 执行: Exec() 用于执行不返回数据的SQL(如 INSERT, UPDATE, DELETE, CREATE)。
  • 关闭: 使用 Close() 方法关闭 RowsStmtDB 对象,释放资源。

代码示例

前置准备

  1. 安装MySQL驱动:
    go get -u github.com/go-sql-driver/mysql
    
  2. 确保你有一个运行的MySQL数据库,并创建一个测试数据库和用户。

完整示例代码

以下示例演示了连接数据库、创建表、执行增删改查操作。

package main

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

	_ "github.com/go-sql-driver/mysql" // 导入驱动,但不在代码中直接使用,所以用 _ 别名
)

// User 结构体用于映射数据库表记录
type User struct {
	ID        int
	Username  string
	Email     string
	CreatedAt time.Time
}

func main() {
	// 1. 连接数据库
	// DSN (Data Source Name) 格式: 用户名:密码@tcp(主机:端口)/数据库名?参数
	dsn := "root:your_password@tcp(127.0.0.1:3306)/testdb?parseTime=true"
	db, err := sql.Open("mysql", dsn)
	if err != nil {
		log.Fatal("连接数据库失败: ", err)
	}
	defer db.Close() // 确保程序结束时关闭数据库连接池

	// 2. 测试连接
	err = db.Ping()
	if err != nil {
		log.Fatal("Ping数据库失败: ", err)
	}
	fmt.Println("成功连接到MySQL数据库!")

	// 3. 配置连接池(可选但推荐)
	db.SetMaxOpenConns(25)                 // 最大打开连接数
	db.SetMaxIdleConns(25)                 // 最大空闲连接数
	db.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期

	// 4. 创建表 (使用Exec)
	createTableSQL := `CREATE TABLE IF NOT EXISTS users (
		id INT AUTO_INCREMENT PRIMARY KEY,
		username VARCHAR(50) NOT NULL UNIQUE,
		email VARCHAR(100) NOT NULL,
		created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
	) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;`

	_, err = db.Exec(createTableSQL)
	if err != nil {
		log.Fatal("创建表失败: ", err)
	}
	fmt.Println("users 表创建成功或已存在。")

	// 5. 插入数据 (使用预编译语句防止SQL注入)
	insertSQL := "INSERT INTO users (username, email) VALUES (?, ?)"
	stmt, err := db.Prepare(insertSQL) // 预编译语句
	if err != nil {
		log.Fatal("准备插入语句失败: ", err)
	}
	defer stmt.Close() // 关闭预编译语句

	// 执行插入
	result, err := stmt.Exec("zhangsan", "[email protected]")
	if err != nil {
		log.Fatal("插入数据失败: ", err)
	}
	id, _ := result.LastInsertId()
	fmt.Printf("插入成功,ID: %d\n", id)

	// 批量插入另一个用户
	_, err = stmt.Exec("lisi", "[email protected]")
	if err != nil {
		log.Fatal("插入数据失败: ", err)
	}
	fmt.Println("批量插入lisi成功。")

	// 6. 查询单行数据 (QueryRow)
	var user User
	queryRowSQL := "SELECT id, username, email, created_at FROM users WHERE username = ?"
	err = db.QueryRow(queryRowSQL, "zhangsan").Scan(
		&user.ID, &user.Username, &user.Email, &user.CreatedAt,
	)
	if err != nil {
		if err == sql.ErrNoRows {
			fmt.Println("未找到用户zhangsan")
		} else {
			log.Fatal("查询单行数据失败: ", err)
		}
	} else {
		fmt.Printf("查询结果: %+v\n", user)
	}

	// 7. 查询多行数据 (Query)
	queryMultiSQL := "SELECT id, username, email FROM users ORDER BY id"
	rows, err := db.Query(queryMultiSQL)
	if err != nil {
		log.Fatal("查询多行数据失败: ", err)
	}
	defer rows.Close() // 非常重要!确保Rows关闭以释放连接

	fmt.Println("\n所有用户:")
	for rows.Next() {
		var u User
		err := rows.Scan(&u.ID, &u.Username, &u.Email)
		if err != nil {
			log.Fatal("扫描行数据失败: ", err)
		}
		fmt.Printf("ID: %d, 用户名: %s, 邮箱: %s\n", u.ID, u.Username, u.Email)
	}
	// 检查遍历过程中是否有错误
	if err = rows.Err(); err != nil {
		log.Fatal("遍历行时出错: ", err)
	}

	// 8. 更新数据 (Exec)
	updateSQL := "UPDATE users SET email = ? WHERE username = ?"
	result, err = db.Exec(updateSQL, "[email protected]", "zhangsan")
	if err != nil {
		log.Fatal("更新数据失败: ", err)
	}
	rowsAffected, _ := result.RowsAffected()
	fmt.Printf("更新成功,影响行数: %d\n", rowsAffected)

	// 9. 删除数据 (Exec)
	deleteSQL := "DELETE FROM users WHERE username = ?"
	result, err = db.Exec(deleteSQL, "lisi")
	if err != nil {
		log.Fatal("删除数据失败: ", err)
	}
	rowsAffected, _ = result.RowsAffected()
	fmt.Printf("删除成功,影响行数: %d\n", rowsAffected)

	// 10. 事务操作示例
	fmt.Println("\n开始事务示例...")
	tx, err := db.Begin() // 开始事务
	if err != nil {
		log.Fatal("开始事务失败: ", err)
	}

	// 在事务中执行多个操作
	_, err = tx.Exec("UPDATE users SET email = ? WHERE username = ?", "[email protected]", "zhangsan")
	if err != nil {
		tx.Rollback() // 如果有错误,回滚事务
		log.Fatal("事务中更新失败: ", err)
	}

	// 假设还需要另一个操作
	// _, err = tx.Exec("INSERT INTO logs ...")
	// if err != nil { tx.Rollback(); ... }

	// 提交事务
	err = tx.Commit()
	if err != nil {
		log.Fatal("提交事务失败: ", err)
	}
	fmt.Println("事务提交成功,数据已持久化。")

	fmt.Println("\n所有操作完成!")
}

实践练习

练习一:基础连接与建表

要求:编写一个Go程序,连接到你的本地MySQL数据库(或任何可用的MySQL实例),创建一个名为products的表,包含字段:id(自增主键)、name(产品名,字符串)、price(价格,浮点数)、stock(库存,整数)。 预期输出:程序运行后打印“连接成功”和“表创建成功”。

练习二:增删改查

要求:在上一题的基础上,扩展你的程序。

  1. products表中插入至少3条产品数据。
  2. 查询并打印所有产品。
  3. 将某个产品的库存增加10。
  4. 删除一个价格低于某个阈值的产品。 预期输出:清晰地展示每一步操作的结果,例如插入的ID、更新影响的行数、删除后剩余的产品列表。

练习三:事务与错误处理

要求:模拟一个“购买商品”场景。

  1. 创建一个orders表(id, product_id, quantity, order_time)。
  2. 开启一个事务。
  3. 在事务中:检查产品的库存是否足够,如果足够,则减少库存并在orders表中插入一条记录;如果库存不足,则回滚事务并打印错误信息。
  4. 尝试使用一个会导致库存不足的数量来测试你的代码。 预期输出:当库存不足时,程序应打印“购买失败:库存不足”,且products表中的库存数量保持不变。

常见错误

  1. 忘记关闭资源db.Query()返回的Rows,以及db.Prepare()返回的Stmt,都必须用defer关闭,否则会导致连接泄漏,最终耗尽连接池。
  2. 不检查错误:数据库操作可能因为各种原因失败(网络、SQL语法、约束冲突等)。忽略错误检查会导致程序在后续产生难以追踪的错误,甚至数据损坏。永远要处理 error 返回值
  3. SQL注入:使用字符串拼接(如fmt.Sprintf("SELECT * FROM users WHERE name='%s'", name))来构建SQL语句是极其危险的。务必使用参数化查询(占位符 ?)和预编译语句
  4. 误用连接池:在查询密集型应用中,将MaxOpenConns设置得太低会导致请求排队等待连接;设置得太高可能压垮数据库。需要根据实际情况调优。
  5. 忽略rows.Err():在使用for rows.Next()循环遍历结果集后,务必检查rows.Err(),以捕获在迭代过程中可能发生的错误。

小结

本节课我们学习了Go语言中操作MySQL数据库的核心技能:

  • 驱动与接口:使用database/sql标准接口配合go-sql-driver/mysql驱动连接MySQL。
  • 连接管理:通过sql.Open创建连接池,并学会配置连接池参数。
  • CRUD操作:分别用Exec执行增删改和建表,用QueryQueryRow进行查询。
  • 安全与效率:掌握使用预编译语句(Prepare)来防止SQL注入并提升重复执行效率。
  • 错误处理与资源释放:强调必须检查所有数据库操作的错误,并使用defer及时关闭RowsStmt以释放资源。
  • 事务处理:学习使用BeginCommitRollback来保证一组数据库操作的原子性和数据一致性。

这些技能构成了Go程序与关系型数据库交互的基础。在下一课中,我们将探索更高级的ORM工具——GORM,它能极大地简化这些操作,但理解底层的database/sql机制将帮助你更好地使用和调试任何ORM框架。

练习编辑器

go
Loading...

继续学习

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