CRUD 操作与预处理语句

CRUD 操作与预处理语句

核心概念

CRUD(Create/Read/Update/Delete)是数据库操作的基本功。在 database/sql 体系下,除了直接拼 SQL 字符串,还有**预处理语句(Prepared Statement)**这一利器。预处理将 SQL 模板和参数分离,带来三个好处:

  1. 安全:参数自动转义,从根本上杜绝 SQL 注入
  2. 性能:同一条 SQL 只解析一次,多次执行复用执行计划
  3. 类型安全:驱动负责 Go 类型到数据库类型的转换

预处理 vs 直接执行

go 复制代码
// 直接执行:每次都要解析 SQL
db.Exec("INSERT INTO users(name, age) VALUES('张三', 25)")

// 预处理:SQL 只解析一次,后续执行只传参数
stmt, _ := db.Prepare("INSERT INTO users(name, age) VALUES(?, ?)")
defer stmt.Close()
stmt.Exec("张三", 25)
stmt.Exec("李四", 32)  // 复用同一个执行计划

NULL 值处理

数据库中的 NULL 是一个独特的值,它不等于空字符串也不等于 0。Go 的基础类型(string, int, float64)无法表达 NULL,直接 Scan 会报错。

方案一:sql.NullXxx 类型

go 复制代码
var name sql.NullString
var age sql.NullInt64
err := db.QueryRow("SELECT name, age FROM users WHERE id = ?", 1).Scan(&name, &age)

if name.Valid {
    fmt.Println(name.String)  // 非 NULL 时可以取值
} else {
    fmt.Println("name is NULL")
}

sql.NullString 是一个结构体:type NullString struct { String string; Valid bool }Valid 为 false 表示数据库值为 NULL。

方案二:使用指针类型

go 复制代码
var name *string
var age *int
db.QueryRow("SELECT name, age FROM users WHERE id = ?", 1).Scan(&name, &age)

if name != nil {
    fmt.Println(*name)  // 解引用
} else {
    fmt.Println("NULL")
}

指针方案更简洁,但每次都要解引用,且 nil 指针解引用会 panic。

方案三:COALESCE 在 SQL 层处理

sql 复制代码
SELECT COALESCE(name, ''), COALESCE(age, 0) FROM users WHERE id = 1

直接在 SQL 里把 NULL 转成默认值,Go 侧用基础类型接收。简单但丢失了"原始值是否为 NULL"的信息。

完整练习代码

go 复制代码
// crud_prepared_statements.go
package main

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

	_ "modernc.org/sqlite"
)

// User 业务结构体
type User struct {
	ID    int64
	Name  string
	Email string
	Age   int
	Bio   string // 可能为 NULL
}

func main() {
	db, err := sql.Open("sqlite", ":memory:")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 建表(bio 字段允许 NULL)
	db.Exec(`CREATE TABLE users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		name TEXT NOT NULL,
		email TEXT UNIQUE NOT NULL,
		age INTEGER DEFAULT 0,
		bio TEXT
	)`)

	// ============================================
	// 1. Create: 预处理批量插入
	// ============================================
	fmt.Println("=== 批量插入(预处理语句) ===")
	insertStmt, err := db.Prepare("INSERT INTO users(name, email, age, bio) VALUES(?, ?, ?, ?)")
	if err != nil {
		log.Fatal(err)
	}
	defer insertStmt.Close()

	users := []User{
		{Name: "张三", Email: "zhangsan@test.com", Age: 28, Bio: "后端工程师"},
		{Name: "李四", Email: "lisi@test.com", Age: 32, Bio: ""}, // Bio 为空字符串
		{Name: "王五", Email: "wangwu@test.com", Age: 25, Bio: ""}, // Bio 为 NULL
	}

	for i, u := range users {
		var bioArg interface{}
		if u.Bio == "" && i == 2 {
			bioArg = nil // 王五的 bio 存为 NULL
		} else {
			bioArg = u.Bio
		}
		res, err := insertStmt.Exec(u.Name, u.Email, u.Age, bioArg)
		if err != nil {
			log.Printf("插入 %s 失败: %v", u.Name, err)
			continue
		}
		id, _ := res.LastInsertId()
		fmt.Printf("  插入成功: ID=%d, %s\n", id, u.Name)
	}

	// ============================================
	// 2. Read: 全量查询 + NULL 处理
	// ============================================
	fmt.Println("\n=== 查询所有用户(NULL 处理) ===")
	allUsers, err := queryAllUsers(db)
	if err != nil {
		log.Fatal(err)
	}
	for _, u := range allUsers {
		fmt.Printf("  [%d] %s <%s> %d岁 | bio: %s\n", u.ID, u.Name, u.Email, u.Age, u.Bio)
	}

	// ============================================
	// 3. Read: 按 ID 查询单个用户
	// ============================================
	fmt.Println("\n=== 按 ID 查询 ===")
	u, err := getUserByID(db, 1)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("  用户: %+v\n", u)

	// 查询不存在
	_, err = getUserByID(db, 999)
	if err == sql.ErrNoRows {
		fmt.Println("  ID=999 的用户不存在(预期行为)")
	}

	// ============================================
	// 4. Update: 更新用户信息
	// ============================================
	fmt.Println("\n=== 更新操作 ===")
	affected, err := updateUserAge(db, 1, 29)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("  更新影响行数: %d\n", affected)

	u, _ = getUserByID(db, 1)
	fmt.Printf("  更新后: %s, %d岁\n", u.Name, u.Age)

	// ============================================
	// 5. Delete: 删除用户
	// ============================================
	fmt.Println("\n=== 删除操作 ===")
	deleted, err := deleteUser(db, 3)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("  删除影响行数: %d\n", deleted)

	// 验证删除结果
	count := countUsers(db)
	fmt.Printf("  剩余用户数: %d\n", count)

	// ============================================
	// 6. 高级查询:条件筛选 + 排序 + 分页
	// ============================================
	fmt.Println("\n=== 分页查询: age > 26, 第1页 ===")
	pageUsers, err := queryUsersWithFilter(db, 26, 10, 0)
	if err != nil {
		log.Fatal(err)
	}
	for _, u := range pageUsers {
		fmt.Printf("  [%d] %s, %d岁\n", u.ID, u.Name, u.Age)
	}
}

// queryAllUsers 查询所有用户,正确处理 NULL 值
func queryAllUsers(db *sql.DB) ([]User, error) {
	rows, err := db.Query("SELECT id, name, email, age, bio FROM users ORDER BY id")
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var users []User
	for rows.Next() {
		var u User
		var bio sql.NullString // 用 sql.NullString 接收可能为 NULL 的字段
		if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age, &bio); err != nil {
			return nil, err
		}
		if bio.Valid {
			u.Bio = bio.String
		} else {
			u.Bio = "(未填写)"
		}
		users = append(users, u)
	}
	return users, rows.Err()
}

// getUserByID 按 ID 查询单个用户
func getUserByID(db *sql.DB, id int64) (User, error) {
	var u User
	var bio sql.NullString
	err := db.QueryRow(
		"SELECT id, name, email, age, bio FROM users WHERE id = ?", id,
	).Scan(&u.ID, &u.Name, &u.Email, &u.Age, &bio)
	if bio.Valid {
		u.Bio = bio.String
	}
	return u, err
}

// updateUserAge 更新用户年龄
func updateUserAge(db *sql.DB, id int64, age int) (int64, error) {
	res, err := db.Exec("UPDATE users SET age = ? WHERE id = ?", age, id)
	if err != nil {
		return 0, err
	}
	return res.RowsAffected()
}

// deleteUser 删除用户
func deleteUser(db *sql.DB, id int64) (int64, error) {
	res, err := db.Exec("DELETE FROM users WHERE id = ?", id)
	if err != nil {
		return 0, err
	}
	return res.RowsAffected()
}

// countUsers 统计用户总数
func countUsers(db *sql.DB) int {
	var count int
	db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
	return count
}

// queryUsersWithFilter 条件筛选 + 分页查询
func queryUsersWithFilter(db *sql.DB, minAge, limit, offset int) ([]User, error) {
	rows, err := db.Query(
		"SELECT id, name, email, age, bio FROM users WHERE age > ? ORDER BY age LIMIT ? OFFSET ?",
		minAge, limit, offset,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	var users []User
	for rows.Next() {
		var u User
		var bio sql.NullString
		if err := rows.Scan(&u.ID, &u.Name, &u.Email, &u.Age, &bio); err != nil {
			return nil, err
		}
		if bio.Valid {
			u.Bio = bio.String
		}
		users = append(users, u)
	}
	return users, rows.Err()
}

运行方式

bash 复制代码
mkdir -p demo2 && cd demo2
go mod init demo2
go get modernc.org/sqlite
go run main.go

要点总结

  1. 预处理语句 db.Prepare() 返回 *sql.Stmt,适合同一条 SQL 反复执行的场景;用完必须 stmt.Close()
  2. NULL 处理三方案sql.NullString 最规范(有 Valid 标志),指针方案最简洁但有 panic 风险,COALESCE 最简单但丢失 NULL 语义
  3. 参数占位符 ?database/sql 的标准写法,驱动负责转义和类型转换,永远不要手动拼接 SQL 字符串
  4. Scan 目标参数顺序必须与 SELECT 字段顺序一一对应,数量不匹配会报错
  5. rows.Err() 必须在循环结束后检查------Next() 返回 false 可能是正常结束,也可能是出错,只有 Err() 能区分
相关推荐
孙启超9 小时前
【AI开发之Rust】第 3 课:字符串与复合类型 —— 数据怎么放
开发语言·人工智能·后端·rust·llm·transformer
a187927218319 小时前
【算法】链表(二):链表上的双指针——变速、异链与定距,和一份路程账本
数据结构·算法·leetcode·链表·go·指针·环形链表
_山海9 小时前
Bun入门指南
前端·javascript·后端
程序员海军10 小时前
AI 越来越强,为什么打工人反而越来越累、越来越内耗了?
前端·程序员·aigc
vx_Biye_Design10 小时前
springboot游泳馆系统93765-计算机课程设计、毕业设计
java·javascript·spring boot·后端·python·spring·课程设计
名字还没想好☜10 小时前
Java NIO ByteBuffer 实战:flip/clear/compact 三个绕晕人的方法与 position/limit 心智模型
java·开发语言·后端·spring·nio
专业程序开发源11 小时前
springbootLivehouse票务系统-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·课程设计·pygame
PC2005_cloud11 小时前
Rust学习笔记:所有权、借用与生命周期——Rust的核心机制
前端·后端
蜗牛互联网11 小时前
语音AI开始边听边说,改变的不只是响应速度
java·人工智能·后端·语音识别
Mav11 小时前
[个人学习记录]从零构建高性能 LLM 推理网关:Go 语言 SSE 流式转发、级联取消与背压
后端