6-GORM 原生 SQL 操作

一、GORM 原生 SQL 核心方法梳理

1. 四大原生 SQL 执行 API

表格

方法 用途 返回值 适用场景
Raw(sql, args...).Scan(&dest) 查询单 / 多条数据(SELECT) Rows、填充结构体 / 切片 复杂联表、自定义聚合、复杂子查询
Exec(sql, args...) 执行 DML/DDL(INSERT/UPDATE/DELETE/CREATE) Result(RowsAffected、Error) 增删改、建索引、建表、存储过程
Row(sql, args...) 查询单行单行数据(标准 database/sql.Row) *sql.Row 只查 1 行简单数据,手动 Scan
Rows(sql, args...) 查询多行原始行(标准 database/sql.Rows) *sql.Rows 超大结果集流式读取,手动遍历

2. 关键安全要点(必看)

  1. 禁止字符串拼接 SQL ,一律用 ? 占位符传参,防止 SQL 注入
  2. Raw/Scan 映射规则:数据库字段名和结构体字段名大小写不敏感,下划线自动匹配驼峰
  3. 执行 DDL 语句(建表、索引、truncate)统一使用 Exec()
  4. 事务内原生 SQL 同样使用 tx.Raw() / tx.Exec()

3. 常用配套能力

  1. 命名参数 db.Raw("SELECT * FROM users WHERE name=@name", sql.Named("name", "张三"))
  2. 获取影响行数 result.RowsAffected
  3. 获取自增 ID(插入后)db.Raw("LAST_INSERT_ID()").Scan(&id)
  4. 事务中执行原生 SQL
  5. 原生 SQL + GORM 链式条件拼接(db.Model(&User{}).Where("age>18").Raw(...)

4. Raw Scan 映射规则

  • 结构体映射:字段名和 sql 查询字段对应
  • 单值接收:var name string; db.Raw("select name from user where id=?",1).Scan(&name)
  • 切片接收多条:var list []User; db.Raw("select * from user").Scan(&list)
  • map 接收单行:var m map[string]interface{}
  • \[\] map 接收多行:var listMap []map[string]interface{}

二、完整可运行示例代码(整合所有原生 SQL 场景)

复制代码
package main

import (
	"database/sql"
	"errors"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
)

// User 测试模型
type User struct {
	gorm.Model
	Name     string  `gorm:"size:32"`
	Age      int     `gorm:"comment:年龄"`
	Email    string  `gorm:"size:64;unique"`
	Score    float64 `gorm:"comment:分数"`
}

var db *gorm.DB

// 初始化数据库
func initDB() {
	dsn := "root:123456@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4&parseTime=True&loc=Local"
	var err error
	db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info), // 打印原生SQL
	})
	if err != nil {
		log.Fatal("连接失败:", err)
	}
	// 自动建表
	db.AutoMigrate(&User{})
	fmt.Println("数据库初始化完成\n")
}

// 1. Raw + Scan 查询:多条映射结构体
func rawQueryStruct() {
	fmt.Println("===== 1.Raw查询多条映射结构体 =====")
	var users []User
	// ? 占位符防注入
	sqlStr := "SELECT id,name,age,score FROM users WHERE age > ? AND score > ?"
	err := db.Raw(sqlStr, 18, 80).Scan(&users).Error
	if err != nil {
		log.Println("查询失败:", err)
		return
	}
	for _, u := range users {
		fmt.Printf("id:%d name:%s age:%d score:%.1f\n", u.ID, u.Name, u.Age, u.Score)
	}
}

// 2. Raw 查询单行、单个字段
func rawQuerySingleValue() {
	fmt.Println("\n===== 2.Raw查询单行单个字段 =====")
	var name string
	var age int
	// 查询单行多字段
	err := db.Raw("SELECT name,age FROM users WHERE id = ?", 1).Scan(&name, &age).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			fmt.Println("无数据")
			return
		}
		log.Println(err)
	}
	fmt.Printf("name:%s age:%d\n", name, age)

	// 只查单个数值
	var count int64
	db.Raw("SELECT COUNT(*) FROM users").Scan(&count)
	fmt.Printf("用户总数:%d\n", count)
}

// 3. Raw 查询 map / []map 动态接收(无结构体)
func rawQueryMap() {
	fmt.Println("\n===== 3.Raw查询map动态接收 =====")
	// 单行 map
	var rowMap map[string]interface{}
	db.Raw("SELECT * FROM users WHERE id = ?", 2).Scan(&rowMap)
	fmt.Println("单行map数据:", rowMap)

	// 多行 []map
	var listMap []map[string]interface{}
	db.Raw("SELECT name,score FROM users WHERE score > ?", 85).Scan(&listMap)
	fmt.Println("多行map列表:", listMap)
}

// 4. 命名参数 sql.Named
func rawNamedParam() {
	fmt.Println("\n===== 4.命名参数查询 =====")
	var u User
	sqlStr := "SELECT * FROM users WHERE name=@uname AND age=@uage"
	err := db.Raw(sqlStr,
		sql.Named("uname", "张三"),
		sql.Named("uage", 20),
	).Scan(&u).Error
	if err == nil {
		fmt.Printf("命名参数查询结果:%+v\n", u)
	}
}

// 5. Row() 单行原始查询(标准sql.Row)
func sqlRowDemo() {
	fmt.Println("\n===== 5.Row()单行原生查询 =====")
	row := db.Raw("SELECT name,score FROM users WHERE id=?", 3).Row()
	var name string
	var score float64
	err := row.Scan(&name, &score)
	if err != nil {
		log.Println(err)
		return
	}
	fmt.Printf("Row查询:%s %.1f\n", name, score)
}

// 6. Rows() 多行流式读取(大数据推荐)
func sqlRowsStream() {
	fmt.Println("\n===== 6.Rows()流式读取大数据 =====")
	rows, err := db.Raw("SELECT id,name,age FROM users").Rows()
	if err != nil {
		log.Println(err)
		return
	}
	defer rows.Close() // 必须关闭,释放连接

	for rows.Next() {
		var id uint
		var name string
		var age int
		if err := rows.Scan(&id, &name, &age); err != nil {
			log.Println(err)
			break
		}
		fmt.Printf("流式行 id:%d name:%s\n", id, name)
	}
}

// 7. Exec 执行DML:INSERT/UPDATE/DELETE
func execDML() {
	fmt.Println("\n===== 7.Exec执行增删改原生SQL =====")
	// 插入
	insertSql := "INSERT INTO users(name,age,email,score) VALUES(?,?,?,?)"
	resInsert := db.Exec(insertSql, "原生SQL测试", 26, "raw@demo.com", 91.2)
	fmt.Printf("插入影响行数:%d\n", resInsert.RowsAffected)

	// 获取自增主键ID
	var newID uint
	db.Raw("SELECT LAST_INSERT_ID()").Scan(&newID)
	fmt.Printf("新增用户ID:%d\n", newID)

	// 更新
	updateSql := "UPDATE users SET score=? WHERE name=?"
	resUpdate := db.Exec(updateSql, 99, "原生SQL测试")
	fmt.Printf("更新影响行数:%d\n", resUpdate.RowsAffected)

	// 删除
	delSql := "DELETE FROM users WHERE name=?"
	resDel := db.Exec(delSql, "原生SQL测试")
	fmt.Printf("删除影响行数:%d\n", resDel.RowsAffected)
}

// 8. Exec 执行DDL语句(建索引、TRUNCATE)
func execDDL() {
	fmt.Println("\n===== 8.Exec执行DDL原生SQL =====")
	// 创建索引
	idxSql := "CREATE INDEX idx_user_name ON users(name)"
	err := db.Exec(idxSql).Error
	if err != nil {
		fmt.Println("索引已存在或创建失败:", err)
	} else {
		fmt.Println("创建name索引成功")
	}

	// 清空表(慎用)
	// db.Exec("TRUNCATE TABLE users")
}

// 9. 事务中执行原生SQL
func rawInTx() {
	fmt.Println("\n===== 9.事务内执行原生SQL =====")
	tx := db.Begin()
	if tx.Error != nil {
		log.Fatal(tx.Error)
	}

	// 事务插入
	tx.Exec("INSERT INTO users(name,age,email,score) VALUES(?,?,?,?)", "事务原生", 22, "txraw@demo.com", 88)
	// 事务更新
	tx.Exec("UPDATE users SET age=? WHERE name=?", 23, "张三")

	// 模拟异常回滚
	// err := errors.New("手动回滚测试")
	// if err != nil {
	// 	tx.Rollback()
	// 	fmt.Println("事务回滚")
	// 	return
	// }

	tx.Commit()
	fmt.Println("事务原生SQL执行提交成功")
}

// 10. GORM链式条件 + Raw混合使用
func rawWithWhereChain() {
	fmt.Println("\n===== 10.链式Where拼接 + Raw =====")
	var list []User
	// 先用Where过滤,再执行自定义Raw
	db.Model(&User{}).Where("age > ?", 18).
		Raw("SELECT id,name,score FROM users WHERE age > ? AND score > 80", 18).
		Scan(&list)
	fmt.Println("混合查询结果:", list)
}

func main() {
	initDB()

	// 查询类原生SQL
	rawQueryStruct()
	rawQuerySingleValue()
	rawQueryMap()
	rawNamedParam()
	sqlRowDemo()
	sqlRowsStream()

	// DML/DDL执行
	execDML()
	execDDL()

	// 事务原生SQL
	rawInTx()

	// 混合链式调用
	rawWithWhereChain()

	fmt.Println("\n===== 所有原生SQL示例执行完毕 =====")
}

三、补充高频使用场景总结

场景 1:联表复杂查询(Raw 首选)

复制代码
type UserJoin struct {
	UserName string
	Score    float64
}
var data []UserJoin
sql := `
SELECT u.name user_name, u.score 
FROM users u 
LEFT JOIN xxx_table t ON u.id = t.user_id 
WHERE u.age > ?
`
db.Raw(sql, 18).Scan(&data)

场景 2:分页原生 SQL

sql

复制代码
SELECT * FROM users WHERE age>? LIMIT ?,?

pageNum := 1
pageSize := 2
offset := (pageNum - 1) * pageSize
var list []User
db.Raw("SELECT * FROM users WHERE age>? LIMIT ?,?", 18, offset, pageSize).Scan(&list)

场景 3:获取执行错误与影响行数

复制代码
res := db.Exec("UPDATE users SET age=?", 20)
if res.Error != nil {
	log.Fatal(res.Error)
}
fmt.Println("影响行数:", res.RowsAffected)

场景 4:禁止 SQL 注入错误示范(不要写)

复制代码
// 错误!字符串拼接,注入风险
name := "张三';drop table users;"
db.Raw(fmt.Sprintf("select * from users where name='%s'", name))

四、依赖安装

复制代码
go get gorm.io/gorm
go get gorm.io/driver/mysql

五、代码覆盖能力清单

  1. Raw 查询映射结构体、单值、map、切片 map
  2. 占位符参数、命名参数 Named
  3. Row 单行查询、Rows 流式多行读取(大数据)
  4. Exec 执行 INSERT/UPDATE/DELETE DML
  5. Exec 执行 CREATE INDEX / TRUNCATE DDL
  6. 事务中执行原生 SQL
  7. GORM Where 链式 + Raw 混合使用
  8. 获取自增 ID、总条数、受影响行数
  9. 防 SQL 注入标准写法
  10. 错误处理、行连接延迟关闭 defer rows.Close ()