GORM ORM 基础

GORM ORM 基础

核心概念

GORM 是 Go 生态中最流行的 ORM 框架。它的设计哲学是:用 Go 结构体描述数据库表,用方法链表达查询逻辑,让开发者用面向对象的方式操作数据库,同时保留逃生通道(Raw SQL)。

ORM vs Raw SQL

go 复制代码
// Raw SQL
rows, _ := db.Query("SELECT id, name, age FROM users WHERE age > ? AND name LIKE ?", 18, "张%")

// GORM
var users []User
db.Where("age > ? AND name LIKE ?", 18, "张%").Find(&users)

ORM 的价值在于:结构体即表结构(减少手写 SQL 的拼写错误)、自动类型转换、关联查询自动化、迁移工具。代价是学习曲线和一定的性能损耗。

连接与初始化

go 复制代码
import (
    "gorm.io/gorm"
    "gorm.io/driver/sqlite"  // 或 mysql, postgres
)

db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})

GORM 的驱动层与 database/sql 不同,它自己封装了连接管理,但底层仍然可以接入 *sql.DB

模型定义

GORM 约定:结构体名蛇形化为表名,字段名蛇形化为列名。

go 复制代码
type User struct {
    ID        uint           `gorm:"primaryKey"`           // 主键
    Name      string         `gorm:"size:100;not null"`    // VARCHAR(100), NOT NULL
    Email     string         `gorm:"uniqueIndex"`          // 唯一索引
    Age       int            `gorm:"default:0"`            // 默认值 0
    Birthday  *time.Time                                     // 指针类型 → 可 NULL
    CreatedAt time.Time                                      // GORM 自动管理创建时间
    UpdatedAt time.Time                                      // GORM 自动管理更新时间
    DeletedAt gorm.DeletedAt `gorm:"index"`                 // 软删除
}

约定优于配置

Go 字段 数据库列 说明
ID id 名为 ID 的字段自动作为主键
CreatedAt created_at 记录创建时自动写入
UpdatedAt updated_at 每次更新自动刷新
DeletedAt deleted_at 软删除标记,查询时自动过滤

标签语法

go 复制代码
type Product struct {
    gorm.Model              // 内嵌 ID + CreatedAt + UpdatedAt + DeletedAt
    Code  string `gorm:"column:product_code;type:varchar(50);uniqueIndex:idx_code"`
    Price uint   `gorm:"check:price > 0"`  // CHECK 约束
}

常用标签:column(列名)、type(列类型)、size(长度)、primaryKeyuniqueIndexindexnot nulldefaultcommentcheck

CRUD 操作

Create

go 复制代码
user := User{Name: "张三", Email: "zs@test.com", Age: 28}
db.Create(&user)  // 插入并回填自增 ID

// 批量插入
db.Create(&[]User{
    {Name: "李四", Email: "ls@test.com", Age: 32},
    {Name: "王五", Email: "ww@test.com", Age: 25},
})

// 选择性插入(只写入指定字段)
db.Select("Name", "Email").Create(&User{Name: "赵六", Email: "zl@test.com", Age: 0})

Read

go 复制代码
// 单条查询
var user User
db.First(&user, 1)                    // 按主键
db.First(&user, "email = ?", "zs@test.com")  // 按条件

// 多条查询
var users []User
db.Find(&users)                       // 全部
db.Where("age >= ?", 25).Find(&users) // 条件
db.Where("name LIKE ?", "张%").Find(&users)

// 排序 + 分页
db.Order("age DESC").Limit(10).Offset(0).Find(&users)

// 计数
var count int64
db.Model(&User{}).Where("age > ?", 20).Count(&count)

Update

go 复制代码
// 更新单个字段
db.Model(&user).Update("age", 29)

// 更新多个字段(struct 方式,零值字段会被忽略)
db.Model(&user).Updates(User{Name: "张三丰", Age: 30})

// 更新多个字段(map 方式,零值也会更新)
db.Model(&user).Updates(map[string]interface{}{"age": 0, "name": "张三"})

// 批量更新
db.Model(&User{}).Where("age < ?", 25).Update("age", 25)

Delete

gorm 复制代码
// 软删除(有 DeletedAt 字段时自动启用)
db.Delete(&user)          // 设置 deleted_at = now()
db.Unscoped().Delete(&user) // 真正删除

// 查询包含已软删除的记录
db.Unscoped().Find(&users)

// 批量删除
db.Where("age < ?", 18).Delete(&User{})

链式调用与方法链

GORM 的查询 API 是链式构建器模式,方法分三类:

类型 方法 说明
链式方法 Where, Order, Limit, Select, Joins 构建查询条件,返回 *gorm.DB
Finisher 方法 Find, First, Create, Update, Delete, Count 触发执行
新会话方法 Session, WithContext, Scopes 创建新会话上下文
go 复制代码
// 链式 + Finisher
db.Where("age > ?", 20).
    Order("created_at DESC").
    Limit(20).
    Offset(0).
    Find(&users)

完整练习代码

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

import (
	"fmt"
	"log"
	"time"

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

// User 用户模型
type User struct {
	ID        uint           `gorm:"primaryKey"`
	Name      string         `gorm:"size:100;not null"`
	Email     string         `gorm:"size:200;uniqueIndex"`
	Age       int            `gorm:"default:0"`
	Birthday  *time.Time
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"`
	Products  []Product      `gorm:"foreignKey:UserID"`
}

// Product 产品模型(演示外键关联)
type Product struct {
	ID    uint `gorm:"primaryKey"`
	Name  string
	Price float64
	Stock int
	// 一对多:一个用户有多个产品
	UserID uint  // 外键
	User   User  `gorm:"foreignKey:UserID"` // 关联
}

func main() {
	// 1. 连接数据库
	db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		log.Fatal(err)
	}

	// 2. 自动迁移(建表)
	if err := db.AutoMigrate(&User{}, &Product{}); err != nil {
		log.Fatal(err)
	}
	fmt.Println("=== 自动迁移完成 ===")

	// ============================================
	// 3. Create
	// ============================================
	fmt.Println("\n=== Create ===")

	// 单条插入
	birthday := time.Date(1998, 5, 15, 0, 0, 0, 0, time.UTC)
	user := User{Name: "张三", Email: "zs@test.com", Age: 28, Birthday: &birthday}
	result := db.Create(&user)
	fmt.Printf("  插入: ID=%d, 影响行数=%d\n", user.ID, result.RowsAffected)

	// 批量插入
	users := []User{
		{Name: "李四", Email: "ls@test.com", Age: 32},
		{Name: "王五", Email: "ww@test.com", Age: 25},
		{Name: "赵六", Email: "zl@test.com", Age: 35},
		{Name: "孙七", Email: "sq@test.com", Age: 22},
	}
	db.Create(&users)
	fmt.Printf("  批量插入: %d 条\n", len(users))

	// ============================================
	// 4. Read
	// ============================================
	fmt.Println("\n=== Read ===")

	// 按主键查询
	var u User
	db.First(&u, 1)
	fmt.Printf("  First(1): %s, %d岁\n", u.Name, u.Age)

	// 条件查询
	var youngUsers []User
	db.Where("age < ?", 30).Order("age ASC").Find(&youngUsers)
	fmt.Printf("  age<30 的用户 (%d人):\n", len(youngUsers))
	for _, yu := range youngUsers {
		fmt.Printf("    %s, %d岁\n", yu.Name, yu.Age)
	}

	// Select 指定字段
	var names []struct {
		Name string
		Age  int
	}
	db.Model(&User{}).Select("name", "age").Where("age >= ?", 28).Scan(&names)
	fmt.Printf("  age>=28 (Select+Scan): ")
	for _, n := range names {
		fmt.Printf("%s(%d) ", n.Name, n.Age)
	}
	fmt.Println()

	// 分页查询
	var pageUsers []User
	var total int64
	db.Model(&User{}).Count(&total)
	db.Limit(2).Offset(0).Order("id").Find(&pageUsers)
	fmt.Printf("  分页(page=1, size=2): 共%d条, 本页%d条\n", total, len(pageUsers))

	// ============================================
	// 5. Update
	// ============================================
	fmt.Println("\n=== Update ===")

	// struct 更新(零值字段被忽略)
	db.Model(&u).Updates(User{Name: "张三丰", Age: 30})
	fmt.Printf("  Updates(struct): %s, %d岁\n", u.Name, u.Age)

	// map 更新(零值也会更新)
	db.Model(&u).Updates(map[string]interface{}{"age": 0})
	db.First(&u, u.ID)
	fmt.Printf("  Updates(map): %s, %d岁 (age=0 生效)\n", u.Name, u.Age)

	// 批量更新
	db.Model(&User{}).Where("age < ?", 25).Update("age", 25)
	var updated []User
	db.Where("age = ?", 25).Find(&updated)
	fmt.Printf("  批量更新 age<25 → age=25: %d人\n", len(updated))

	// ============================================
	// 6. Delete(软删除)
	// ============================================
	fmt.Println("\n=== Delete(软删除) ===")

	var zhao User
	db.Where("name = ?", "赵六").First(&zhao)
	db.Delete(&zhao)

	// 普通查询看不到已删除的
	var count int64
	db.Model(&User{}).Count(&count)
	fmt.Printf("  删除赵六后,剩余用户数: %d\n", count)

	// Unscoped 可以查到软删除的记录
	var allUsers []User
	db.Unscoped().Find(&allUsers)
	fmt.Printf("  Unscoped 查询(含软删除): %d条\n", len(allUsers))

	// ============================================
	// 7. 关联关系(一对多)
	// ============================================
	fmt.Println("\n=== 关联关系 ===")

	// 为张三创建产品
	db.Create(&Product{Name: "Go编程书", Price: 89.5, Stock: 100, UserID: 1})
	db.Create(&Product{Name: "机械键盘", Price: 459.0, Stock: 50, UserID: 1})
	db.Create(&Product{Name: "鼠标", Price: 99.0, Stock: 200, UserID: 2})

	// 预加载查询(避免 N+1 问题)
	var userWithProducts User
	db.Preload("Products").First(&userWithProducts, 1)
	fmt.Printf("  %s 的产品 (%d件):\n", userWithProducts.Name, len(userWithProducts.Products))
	for _, p := range userWithProducts.Products {
		fmt.Printf("    %s: ¥%.2f (库存%d)\n", p.Name, p.Price, p.Stock)
	}

	// ============================================
	// 8. 钩子(Hooks)
	// ============================================
	fmt.Println("\n=== Hooks 演示 ===")

	// BeforeCreate 钩子会在插入前自动调用
	newUser := User{Name: "测试钩子", Email: "hook@test.com", Age: -1}
	db.Create(&newUser)
	fmt.Printf("  创建前 Age=-1, 创建后: %d (钩子修正了负数)\n", newUser.Age)
}

// BeforeCreate GORM 钩子:插入前自动调用
func (u *User) BeforeCreate(tx *gorm.DB) (err error) {
	// 年龄不能为负数,自动修正为 0
	if u.Age < 0 {
		u.Age = 0
	}
	return nil
}

运行方式

bash 复制代码
mkdir -p demo4 && cd demo4
go mod init demo4
go get gorm.io/gorm gorm.io/driver/sqlite
go run main.go

要点总结

  1. GORM 用结构体描述表 ,标签控制列属性;gorm.Model 内嵌了 ID + 时间戳 + 软删除四件套
  2. 链式方法构建条件 ,Finisher 方法触发执行;链式方法返回 *gorm.DB 可继续链式调用
  3. struct Updates 零值被忽略 ,需要更新零值时用 map[string]interface{}
  4. 软删除通过 DeletedAt 实现 ,查询自动过滤;Unscoped() 可查已删除记录
  5. Preload 解决 N+1 问题:一条 SQL JOIN 加载关联数据,避免循环查询
  6. Hooks 在生命周期点自动触发BeforeCreate/AfterCreate/BeforeUpdate/BeforeDelete
  7. AutoMigrate 只增不减:新增字段和表会自动创建,但删除字段需要手动操作或用迁移工具
相关推荐
小岛前端1 小时前
AI Skills 已经封神,但新的问题却越来越严重!
前端·后端·github
拖孩1 小时前
一个人 + AI 做的小程序,上线 15 天赚了 10 块 5
前端·后端·微信小程序
newerp1 小时前
CRUD 操作与预处理语句
后端·程序员·go
wei_shuo1 小时前
KES 数据同步与ETL实战:数据集成、转换与实时同步方案
后端
乒乓狂魔14786739970001 小时前
LangGraph:用一张图,编排一群 AI 助手
后端
Gopher_HBo1 小时前
请求绑定 binding(binding/ 包)
后端
Cache技术分享2 小时前
504. Java 反射 - 创建一个简单的依赖注入框架
前端·后端
Java内核笔记2 小时前
Spring Boot 4 虚拟线程源码剖析:Tomcat 线程池是怎么被换掉的
spring boot·后端
掘金码甲哥2 小时前
WorkBuddy 直连 DeepSeek v4 Flash,一发截图就崩?我们在网关层一招根治
后端