Skip to content

02-CRUD 操作与查询基础

CRUD(Create、Read、Update、Delete)是任何 ORM 的核心能力。GORM 提供了链式 API 让数据库操作变得直观且类型安全。本篇将系统讲解 GORM 的增删改查操作、各种查询条件、聚合函数、软删除等核心知识,并通过完整的用户管理示例巩固所学。

创建记录

Create 单条记录

使用 Create 方法插入一条记录,GORM 会自动填充 ID、CreatedAt 等字段。

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID     uint   `gorm:"primaryKey"`
	Name   string `gorm:"type:varchar(50)"`
	Email  string `gorm:"type:varchar(150);uniqueIndex"`
	Age    int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 创建单条记录
	user := User{Name: "张三", Email: "zhangsan@example.com", Age: 25}
	result := db.Create(&user)

	fmt.Printf("影响行数: %d\n", result.RowsAffected)
	fmt.Printf("自增 ID: %d\n", user.ID) // Create 后 ID 会被自动填充
	fmt.Printf("错误: %v\n", result.Error)
}

批量创建

Create 也支持传入切片进行批量插入:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string `gorm:"type:varchar(50)"`
	Email string `gorm:"type:varchar(150);uniqueIndex"`
	Age   int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	users := []User{
		{Name: "李四", Email: "lisi@example.com", Age: 30},
		{Name: "王五", Email: "wangwu@example.com", Age: 28},
		{Name: "赵六", Email: "zhaoliu@example.com", Age: 35},
	}
	result := db.Create(&users)
	fmt.Printf("批量插入 %d 条记录\n", result.RowsAffected)

	// 查询所有 ID
	for _, u := range users {
		fmt.Printf("ID: %d, Name: %s\n", u.ID, u.Name)
	}
}

指定字段创建

如果只想插入部分字段,可以使用 Select 指定:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID     uint   `gorm:"primaryKey"`
	Name   string `gorm:"type:varchar(50)"`
	Email  string `gorm:"type:varchar(150);uniqueIndex"`
	Age    int
	Status int `gorm:"default:1"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 只插入 Name 和 Email 字段,其他字段使用默认值
	user := User{Name: "Tom", Email: "tom@example.com", Age: 20, Status: 0}
	db.Select("Name", "Email").Create(&user)

	// 查询验证
	var found User
	db.First(&found, user.ID)
	fmt.Printf("Age(默认0): %d, Status(默认1): %d\n", found.Age, found.Status)
}

读取记录

First、Take、Last

  • First:按主键升序取第一条
  • Take:不排序取第一条
  • Last:按主键降序取第一条
go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string `gorm:"type:varchar(50)"`
	Email string `gorm:"type:varchar(150);uniqueIndex"`
	Age   int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 准备数据
	db.Create(&User{Name: "A", Email: "a@x.com", Age: 20})
	db.Create(&User{Name: "B", Email: "b@x.com", Age: 25})
	db.Create(&User{Name: "C", Email: "c@x.com", Age: 30})

	var firstUser, takeUser, lastUser User
	db.First(&firstUser)        // SELECT * FROM users ORDER BY id LIMIT 1
	db.Take(&takeUser)          // SELECT * FROM users LIMIT 1
	db.Last(&lastUser)          // SELECT * FROM users ORDER BY id DESC LIMIT 1

	fmt.Printf("First: %s\n", firstUser.Name)
	fmt.Printf("Take: %s\n", takeUser.Name)
	fmt.Printf("Last: %s\n", lastUser.Name)

	// 按主键查询
	var byID User
	db.First(&byID, 2) // SELECT * FROM users WHERE id = 2
	fmt.Printf("ID=2: %s\n", byID.Name)

	// 按主键切片查询
	var users []User
	db.Find(&users, []int{1, 2, 3})
	fmt.Printf("按 ID 切片查询到 %d\n", len(users))
}

重要提示FirstTakeLast 找不到记录时会返回 ErrRecordNotFound 错误;而 Find 不会。

Find 查询多条

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string
	Email string
	Age   int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	var users []User
	result := db.Find(&users) // SELECT * FROM users
	fmt.Printf("查询到 %d 条记录\n", result.RowsAffected)

	// Find 不会返回 ErrRecordNotFound
	emptyResult := db.Where("1 = 0").Find(&users)
	fmt.Printf("空查询错误: %v, 行数: %d\n", emptyResult.Error, emptyResult.RowsAffected)
}

查询条件

字符串条件

最灵活的查询方式,但要注意 SQL 注入风险(使用 ? 占位符避免):

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string
	Email string
	Age   int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	db.Create(&User{Name: "Tom", Email: "tom@x.com", Age: 25})

	var users []User

	// 等值条件
	db.Where("name = ?", "Tom").Find(&users)
	fmt.Printf("name=Tom: %d\n", len(users))

	// 比较条件
	db.Where("age > ?", 20).Find(&users)
	fmt.Printf("age>20: %d\n", len(users))

	// IN 条件
	db.Where("name IN ?", []string{"Tom", "Jerry"}).Find(&users)
	fmt.Printf("name IN (Tom, Jerry): %d\n", len(users))

	// LIKE 模糊查询
	db.Where("name LIKE ?", "%o%").Find(&users)
	fmt.Printf("name LIKE %%o%%: %d\n", len(users))

	// BETWEEN
	db.Where("age BETWEEN ? AND ?", 20, 30).Find(&users)
	fmt.Printf("age BETWEEN 20 AND 30: %d\n", len(users))

	// AND / OR 组合
	db.Where("name = ? AND age > ?", "Tom", 20).Find(&users)
	fmt.Printf("name=Tom AND age>20: %d\n", len(users))
}

结构体条件

使用结构体作为条件,零值字段会被忽略:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string
	Email string
	Age   int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	db.Create(&User{Name: "Tom", Email: "tom@x.com", Age: 25})
	db.Create(&User{Name: "Tom", Email: "tom2@x.com", Age: 0})

	var users []User

	// 结构体条件:零值字段被忽略,Age=0 不会作为条件
	db.Where(&User{Name: "Tom", Age: 0}).Find(&users)
	fmt.Printf("结构体条件(Age=0被忽略): %d\n", len(users))
	// 这里会查询到所有 Name=Tom 的记录,包括 Age=0 的
}

Map 条件

Map 条件会包含零值字段,适合需要精确匹配零值的场景:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string
	Email string
	Age   int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	db.Create(&User{Name: "Tom", Email: "tom@x.com", Age: 25})
	db.Create(&User{Name: "Tom", Email: "tom2@x.com", Age: 0})

	var users []User

	// Map 条件:Age=0 会作为查询条件
	db.Where(map[string]interface{}{"name": "Tom", "age": 0}).Find(&users)
	fmt.Printf("Map 条件(Age=0作为条件): %d\n", len(users))
	// 这里只查询到 Name=Tom AND Age=0 的记录
}

选择字段 Select

Select 指定查询哪些字段,减少数据传输量:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string
	Email string
	Age   int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})
	db.Create(&User{Name: "Tom", Email: "tom@x.com", Age: 25})

	// 只查询 name 和 age
	var users []User
	db.Select("name", "age").Find(&users)
	for _, u := range users {
		fmt.Printf("Name: %s, Age: %d, Email: %s\n", u.Name, u.Age, u.Email)
		// Email 为空,因为没有查询
	}

	// 排除字段(使用 Omit)
	db.Omit("email", "age").Find(&users)
	for _, u := range users {
		fmt.Printf("Name: %s\n", u.Name)
	}
}

排序 Order

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID    uint   `gorm:"primaryKey"`
	Name  string
	Age   int
	Score float64
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	db.Create(&User{Name: "A", Age: 20, Score: 90.5})
	db.Create(&User{Name: "B", Age: 25, Score: 85.0})
	db.Create(&User{Name: "C", Age: 30, Score: 95.0})

	var users []User

	// 按 age 降序
	db.Order("age desc").Find(&users)
	fmt.Println("按 age 降序:")
	for _, u := range users {
		fmt.Printf("  %s: %d\n", u.Name, u.Age)
	}

	// 多字段排序
	db.Order("score desc").Order("age asc").Find(&users)
	fmt.Println("按 score 降序、age 升序:")
	for _, u := range users {
		fmt.Printf("  %s: score=%.1f age=%d\n", u.Name, u.Score, u.Age)
	}
}

分页 Limit + Offset

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint   `gorm:"primaryKey"`
	Name string
	Age  int
}

func page(db *gorm.DB, page, size int) *gorm.DB {
	if page <= 0 {
		page = 1
	}
	if size <= 0 || size > 100 {
		size = 10
	}
	offset := (page - 1) * size
	return db.Offset(offset).Limit(size)
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	// 准备 15 条数据
	for i := 1; i <= 15; i++ {
		db.Create(&User{Name: fmt.Sprintf("user_%d", i), Age: 20 + i})
	}

	// 第一页,每页 10 条
	var users []User
	page(db.Model(&User{}), 1, 10).Find(&users)
	fmt.Printf("第 1 页: %d\n", len(users))

	// 第二页
	page(db.Model(&User{}), 2, 10).Find(&users)
	fmt.Printf("第 2 页: %d\n", len(users))

	// 第三页(只有 5 条)
	page(db.Model(&User{}), 3, 10).Find(&users)
	fmt.Printf("第 3 页: %d\n", len(users))

	// 取消 Limit 和 Offset(在同一事务中复用 db 时有用)
	db.Limit(10).Find(&users).Limit(-1).Find(&users)
}

分组与聚合

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type Order struct {
	ID       uint    `gorm:"primaryKey"`
	UserName string  `gorm:"type:varchar(50);index"`
	Amount   float64
	Status   string `gorm:"type:varchar(20)"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&Order{})

	// 准备数据
	orders := []Order{
		{UserName: "Tom", Amount: 100, Status: "paid"},
		{UserName: "Tom", Amount: 200, Status: "paid"},
		{UserName: "Jerry", Amount: 150, Status: "paid"},
		{UserName: "Jerry", Amount: 50, Status: "unpaid"},
		{UserName: "Tom", Amount: 80, Status: "unpaid"},
	}
	db.Create(&orders)

	type Result struct {
		UserName  string
		Total     float64
		Count     int64
		PaidTotal float64
	}

	// Group + Having + 聚合函数
	var results []Result
	db.Model(&Order{}).
		Select("user_name, SUM(amount) as total, COUNT(*) as count, SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) as paid_total").
		Group("user_name").
		Having("COUNT(*) > ?", 1).
		Scan(&results)

	for _, r := range results {
		fmt.Printf("用户: %s, 总额: %.0f, 订单数: %d, 已付: %.0f\n",
			r.UserName, r.Total, r.Count, r.PaidTotal)
	}

	// 单独的 Count
	var count int64
	db.Model(&Order{}).Where("status = ?", "paid").Count(&count)
	fmt.Printf("已支付订单数: %d\n", count)

	// 单独的 Sum
	var sum float64
	db.Model(&Order{}).Where("status = ?", "paid").Sum("amount", &sum)
	fmt.Printf("已支付总额: %.0f\n", sum)
}

去重 Distinct

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint   `gorm:"primaryKey"`
	Name string
	City string
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	db.Create(&User{Name: "A", City: "Beijing"})
	db.Create(&User{Name: "B", City: "Shanghai"})
	db.Create(&User{Name: "C", City: "Beijing"})

	// 查询不重复的城市
	var cities []string
	db.Model(&User{}).Distinct("city").Pluck("city", &cities)
	fmt.Println("不重复的城市:", cities)

	// 多字段 Distinct
	type CityUser struct {
		City string
		Name string
	}
	var cityUsers []CityUser
	db.Model(&User{}).Distinct("city", "name").Scan(&cityUsers)
	fmt.Println("城市+用户组合数:", len(cityUsers))
}

更新记录

Save 保存整个对象

Save 会保存所有字段,包括零值字段:

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID     uint   `gorm:"primaryKey"`
	Name   string
	Email  string
	Age    int
	Status int `gorm:"default:1"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	user := User{Name: "Tom", Email: "tom@x.com", Age: 25, Status: 1}
	db.Create(&user)

	// Save 会保存所有字段
	user.Age = 26
	user.Status = 0 // 零值也会被保存
	db.Save(&user)

	var found User
	db.First(&found, user.ID)
	fmt.Printf("Age: %d, Status: %d\n", found.Age, found.Status)
}

Update 单字段

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID     uint   `gorm:"primaryKey"`
	Name   string
	Email  string
	Age    int
	Status int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	user := User{Name: "Tom", Email: "tom@x.com", Age: 25, Status: 1}
	db.Create(&user)

	// Update 单个字段
	db.Model(&user).Update("age", 30)
	fmt.Printf("Update 后 Age: %d\n", user.Age)

	// 条件更新
	db.Model(&User{}).Where("name = ?", "Tom").Update("status", 0)

	var found User
	db.First(&found, user.ID)
	fmt.Printf("Age: %d, Status: %d\n", found.Age, found.Status)
}

Updates 多字段

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID     uint   `gorm:"primaryKey"`
	Name   string
	Email  string
	Age    int
	Status int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	user := User{Name: "Tom", Email: "tom@x.com", Age: 25, Status: 1}
	db.Create(&user)

	// 结构体更新:零值字段被忽略
	db.Model(&user).Updates(struct {
		Age    int
		Status int
	}{Age: 30, Status: 0})
	// Status=0 不会被更新,因为是零值

	var found User
	db.First(&found, user.ID)
	fmt.Printf("结构体更新后 Age: %d, Status: %d(零值被忽略)\n", found.Age, found.Status)
}

更新零值问题

结构体更新会忽略零值字段,这是常见陷阱。解决方法:使用 map 更新,或者用 Select 指定要更新的字段。

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID     uint   `gorm:"primaryKey"`
	Name   string
	Email  string
	Age    int
	Status int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	user := User{Name: "Tom", Email: "tom@x.com", Age: 25, Status: 1}
	db.Create(&user)

	// 方法1:使用 Map 更新,零值会被写入
	db.Model(&user).Updates(map[string]interface{}{
		"age":    30,
		"status": 0, // 零值也会被更新
	})

	var found1 User
	db.First(&found1, user.ID)
	fmt.Printf("Map 更新后 Status: %d\n", found1.Status)

	// 方法2:使用 Select 指定字段
	db.Model(&user).Select("status").Updates(struct{ Status int }{Status: 1})
	var found2 User
	db.First(&found2, user.ID)
	fmt.Printf("Select 更新后 Status: %d\n", found2.Status)
}

删除记录

Delete 单条删除

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint   `gorm:"primaryKey"`
	Name string
	Age  int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	db.Create(&User{Name: "Tom", Age: 25})

	var user User
	db.First(&user, 1)

	// 删除单条
	db.Delete(&user)

	var count int64
	db.Model(&User{}).Count(&count)
	fmt.Printf("删除后剩余: %d\n", count)
}

批量删除

go
package main

import (
	"fmt"
	"log"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID   uint   `gorm:"primaryKey"`
	Name string
	Age  int
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	for i := 1; i <= 10; i++ {
		db.Create(&User{Name: fmt.Sprintf("user_%d", i), Age: 20 + i})
	}

	// 条件批量删除
	result := db.Where("age > ?", 25).Delete(&User{})
	fmt.Printf("删除 %d\n", result.RowsAffected)

	// 按主键切片删除
	db.Delete(&User{}, []int{1, 2, 3})
}

软删除与 Unscoped

如果模型包含 gorm.DeletedAt 字段,Delete 不会真正删除记录,而是设置 DeletedAt。查询时默认会过滤掉已软删除的记录。

go
package main

import (
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint           `gorm:"primaryKey"`
	Name      string
	Age       int
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("crud.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	db.Create(&User{Name: "Tom", Age: 25})
	db.Create(&User{Name: "Jerry", Age: 30})

	// 软删除 Tom
	var tom User
	db.Where("name = ?", "Tom").First(&tom)
	db.Delete(&tom)

	// 普通查询:找不到已删除的
	var activeUsers []User
	db.Find(&activeUsers)
	fmt.Printf("活跃用户数: %d(只剩 Jerry)\n", len(activeUsers))

	// Unscoped:查询所有,包括软删除的
	var allUsers []User
	db.Unscoped().Find(&allUsers)
	fmt.Printf("所有用户数: %d(包含 Tom)\n", len(allUsers))

	// Unscoped:永久删除
	db.Unscoped().Where("name = ?", "Tom").Delete(&User{})

	var finalUsers []User
	db.Unscoped().Find(&finalUsers)
	fmt.Printf("永久删除后用户数: %d\n", len(finalUsers))

	_ = time.Now
}

完整示例:用户管理 CRUD

下面用一个完整的用户管理示例综合演示 CRUD 全流程:

go
package main

import (
	"errors"
	"fmt"
	"log"
	"time"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint           `gorm:"primaryKey" json:"id"`
	Name      string         `gorm:"type:varchar(50);not null;index" json:"name"`
	Email     string         `gorm:"type:varchar(150);uniqueIndex;not null" json:"email"`
	Age       int            `gorm:"default:0" json:"age"`
	Status    int            `gorm:"default:1" json:"status"` // 1启用 0禁用
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

type UserService struct {
	db *gorm.DB
}

func NewUserService(db *gorm.DB) *UserService {
	return &UserService{db: db}
}

// Create 创建用户
func (s *UserService) Create(name, email string, age int) (*User, error) {
	user := &User{Name: name, Email: email, Age: age, Status: 1}
	if err := s.db.Create(user).Error; err != nil {
		return nil, fmt.Errorf("创建用户失败: %w", err)
	}
	return user, nil
}

// GetByID 根据 ID 查询
func (s *UserService) GetByID(id uint) (*User, error) {
	var user User
	if err := s.db.First(&user, id).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, fmt.Errorf("用户不存在")
		}
		return nil, err
	}
	return &user, nil
}

// List 分页查询
func (s *UserService) List(page, size int, keyword string) ([]User, int64, error) {
	if page <= 0 {
		page = 1
	}
	if size <= 0 || size > 100 {
		size = 10
	}
	offset := (page - 1) * size

	var users []User
	var total int64

	query := s.db.Model(&User{})
	if keyword != "" {
		query = query.Where("name LIKE ?", "%"+keyword+"%")
	}

	query.Count(&total)
	if err := query.Order("id desc").Offset(offset).Limit(size).Find(&users).Error; err != nil {
		return nil, 0, err
	}
	return users, total, nil
}

// Update 更新用户(使用 map 避免零值问题)
func (s *UserService) Update(id uint, updates map[string]interface{}) error {
	result := s.db.Model(&User{}).Where("id = ?", id).Updates(updates)
	if result.Error != nil {
		return result.Error
	}
	if result.RowsAffected == 0 {
		return fmt.Errorf("用户不存在或无更新")
	}
	return nil
}

// Disable 禁用用户
func (s *UserService) Disable(id uint) error {
	return s.Update(id, map[string]interface{}{"status": 0})
}

// Enable 启用用户
func (s *UserService) Enable(id uint) error {
	return s.Update(id, map[string]interface{}{"status": 1})
}

// Delete 软删除
func (s *UserService) Delete(id uint) error {
	return s.db.Delete(&User{}, id).Error
}

// HardDelete 永久删除
func (s *UserService) HardDelete(id uint) error {
	return s.db.Unscoped().Delete(&User{}, id).Error
}

func main() {
	db, err := gorm.Open(sqlite.Open("user_mgr.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	db.AutoMigrate(&User{})

	svc := NewUserService(db)

	// 创建
	u1, _ := svc.Create("张三", "zhangsan@example.com", 25)
	u2, _ := svc.Create("李四", "lisi@example.com", 30)
	u3, _ := svc.Create("王五", "wangwu@example.com", 28)
	fmt.Printf("创建用户: %s(id=%d), %s(id=%d), %s(id=%d)\n",
		u1.Name, u1.ID, u2.Name, u2.ID, u3.Name, u3.ID)

	// 查询单个
	found, _ := svc.GetByID(u1.ID)
	fmt.Printf("查询到: %s, 邮箱: %s\n", found.Name, found.Email)

	// 分页查询
	users, total, _ := svc.List(1, 2, "")
	fmt.Printf("分页查询第1页: 共%d条, 当前页%d\n", total, len(users))
	for _, u := range users {
		fmt.Printf("  - %s (age=%d)\n", u.Name, u.Age)
	}

	// 关键字搜索
	searched, _, _ := svc.List(1, 10, "张")
	fmt.Printf("搜索'张': %d\n", len(searched))

	// 更新
	svc.Update(u1.ID, map[string]interface{}{"age": 26, "status": 0})
	updated, _ := svc.GetByID(u1.ID)
	fmt.Printf("更新后: age=%d, status=%d\n", updated.Age, updated.Status)

	// 禁用 / 启用
	svc.Enable(u1.ID)
	updated, _ = svc.GetByID(u1.ID)
	fmt.Printf("启用后 status=%d\n", updated.Status)

	// 软删除
	svc.Delete(u3.ID)
	users, total, _ = svc.List(1, 10, "")
	fmt.Printf("软删除后: 共%d\n", total)

	// Unscoped 查询软删除
	var allUsers []User
	db.Unscoped().Find(&allUsers)
	fmt.Printf("包含软删除: %d\n", len(allUsers))
}

小结

本篇系统讲解了 GORM 的 CRUD 操作:

  1. 创建Create 支持单条、批量、指定字段创建
  2. 读取First/Take/Last/Find,注意 ErrRecordNotFound 的差异
  3. 查询条件:字符串、结构体(忽略零值)、Map(包含零值)三种方式
  4. Select/Omit:精确控制查询字段,减少数据传输
  5. Order/Limit/Offset:排序与分页
  6. Group/Having/Count/Sum:分组聚合统计
  7. Distinct:去重查询
  8. 更新Save 全字段、Update 单字段、Updates 多字段
  9. 零值陷阱:结构体更新会忽略零值,使用 mapSelect 解决
  10. 删除:默认软删除,Unscoped 查询或永久删除

下一篇我们将深入 GORM 的关联关系,包括一对一、一对多、多对多及预加载等高级特性。