GORM:Go 的 ORM 利器

学习使用 GORM,Go 语言最流行的 ORM 框架,简化数据库操作

GORM:Go 的 ORM 利器

在之前的文章中,我们学习了如何使用 database/sql 直接操作数据库。虽然这种方式灵活且性能高,但在实际项目中,大量的 CRUD 操作写起来非常繁琐,而且容易出错。

这时候,ORM(Object-Relational Mapping,对象关系映射)就派上用场了。ORM 把数据库的表映射成代码中的结构体,把 SQL 操作映射成方法调用,让数据库操作变得简单而安全。

GORM 是 Go 语言中最流行的 ORM 框架,它功能强大、API 友好、性能优异。今天我们就来学习如何使用 GORM。

安装 GORM

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql  # 或 sqlite, postgres, sqlserver

连接数据库

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func main() {
	dsn := "root:password@tcp(localhost:3306)/myapp?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("连接数据库失败")
	}
	
	// 测试连接
	sqlDB, _ := db.DB()
	sqlDB.Ping()
	
	println("数据库连接成功!")
}

定义模型

GORM 使用结构体来定义模型。通过标签(tags)可以配置表名、字段名、约束等:

package main

import (
	"time"
	
	"gorm.io/gorm"
)

// User 用户模型
type User struct {
	ID        uint           `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除
	
	// 自定义字段
	Name     string `gorm:"type:varchar(100);not null;comment:用户名"`
	Email    string `gorm:"uniqueIndex;not null;comment:邮箱"`
	Age      int    `gorm:"default:0;comment:年龄"`
	IsActive bool   `gorm:"default:true;comment:是否激活"`
}

// TableName 自定义表名
func (User) TableName() string {
	return "users"
}

常用标签

标签说明例子
column列名gorm:"column:user_name"
type数据类型gorm:"type:varchar(100)"
size大小gorm:"size:255"
primaryKey主键gorm:"primaryKey"
unique唯一索引gorm:"unique"
default默认值gorm:"default:0"
not null非空gorm:"not null"
autoIncrement自增gorm:"autoIncrement"
comment注释gorm:"comment:用户邮箱"

自动迁移

GORM 可以根据模型自动创建或更新表结构:

// 自动迁移(只会创建表、添加缺失的列和索引,不会删除或修改已有列)
db.AutoMigrate(&User{})

// 检查表是否存在
db.Migrator().HasTable(&User{})

// 删除表
db.Migrator().DropTable(&User{})

⚠️ 注意AutoMigrate 只会在开发环境使用。生产环境应该使用专业的迁移工具(如 golang-migrate)。

CRUD 操作

Create(创建)

// 创建单条记录
user := User{Name: "张三", Email: "zhangsan@example.com", Age: 25}
db.Create(&user)
fmt.Println("用户ID:", user.ID)  // 自动回填 ID

// 批量创建
users := []User{
	{Name: "李四", Email: "lisi@example.com", Age: 30},
	{Name: "王五", Email: "wangwu@example.com", Age: 28},
}
db.Create(&users)

// 指定字段创建
db.Select("Name", "Email").Create(&user)

// 忽略某些字段
db.Omit("Age").Create(&user)

// 使用 Map 创建
db.Model(&User{}).Create(map[string]interface{}{
	"Name":  "赵六",
	"Email": "zhaoliu@example.com",
	"Age":   35,
})

Read(查询)

// 查询单条记录
var user User
db.First(&user)                          // 按主键升序找第一个
db.Last(&user)                           // 按主键降序找第一个
db.Take(&user)                           // 不指定顺序
db.First(&user, 1)                       // 主键 = 1
db.First(&user, "name = ?", "张三")       // 条件查询

// 查询多条记录
var users []User
db.Find(&users)                          // 查询所有
db.Find(&users, []int{1, 2, 3})          // 主键 IN (1,2,3)
db.Find(&users, "age > ?", 20)           // 条件查询

// 条件查询
db.Where("name = ?", "张三").First(&user)
db.Where("name LIKE ?", "%张%").Find(&users)
db.Where("age BETWEEN ? AND ?", 20, 30).Find(&users)
db.Where("name IN ?", []string{"张三", "李四"}).Find(&users)

// 链式查询
db.Where("age > ?", 20).
	Where("is_active = ?", true).
	Order("age desc").
	Limit(10).
	Offset(0).
	Find(&users)

// Or 条件
db.Where("age > ?", 20).Or("is_active = ?", true).Find(&users)

// 选择特定字段
db.Select("name", "age").Find(&users)
db.Select([]string{"name", "age"}).Find(&users)

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

// Pluck(获取单列)
var names []string
db.Model(&User{}).Pluck("name", &names)

// Scan(扫描到自定义结构体)
type Result struct {
	Name string
	Age  int
}
var results []Result
db.Model(&User{}).Select("name", "age").Find(&results)

Update(更新)

// 更新单条记录
db.First(&user)
user.Name = "张三(已更新)"
user.Age = 26
db.Save(&user)  // 保存所有字段

// 更新特定字段
db.Model(&user).Update("name", "新名字")
db.Model(&user).Updates(User{Name: "新名字", Age: 27})
db.Model(&user).Updates(map[string]interface{}{
	"name": "新名字",
	"age":  27,
})

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

// 使用 SQL 表达式更新
db.Model(&User{}).Update("age", gorm.Expr("age + 1"))

Delete(删除)

// 软删除(默认,设置 deleted_at 字段)
db.Delete(&user)
db.Delete(&User{}, 1)  // 主键 = 1
db.Where("age < ?", 18).Delete(&User{})

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

// 永久删除(物理删除)
db.Unscoped().Delete(&user)

// 恢复软删除的记录
db.Unscoped().Model(&user).Update("deleted_at", nil)

关联关系

Has One(一对一)

type User struct {
	ID   uint
	Name string
}

type Profile struct {
	ID     uint
	UserID uint   // 外键
	User   User   // 关联
	Bio    string
}

// 查询时预加载
db.Preload("User").Find(&profiles)

Has Many(一对多)

type User struct {
	ID    uint
	Name  string
	Posts []Post  // 关联
}

type Post struct {
	ID     uint
	UserID uint
	User   User
	Title  string
}

// 查询用户及其文章
var user User
db.Preload("Posts").First(&user, 1)
fmt.Println("用户的文章:", len(user.Posts))

// 嵌套预加载
db.Preload("Posts.Comments").Find(&users)

Many to Many(多对多)

type User struct {
	ID    uint
	Name  string
	Roles []Role `gorm:"many2many:user_roles;"`
}

type Role struct {
	ID    uint
	Name  string
	Users []User `gorm:"many2many:user_roles;"`
}

// 查询用户及其角色
db.Preload("Roles").First(&user)

// 添加关联
db.Model(&user).Association("Roles").Append(&role)

// 删除关联
db.Model(&user).Association("Roles").Delete(&role)

// 替换关联
db.Model(&user).Association("Roles").Replace(&role1, &role2)

事务

// 手动事务
tx := db.Begin()

// 执行操作
if err := tx.Create(&user).Error; err != nil {
	tx.Rollback()
	return err
}

if err := tx.Create(&profile).Error; err != nil {
	tx.Rollback()
	return err
}

return tx.Commit().Error

// 使用闭包(推荐)
err := db.Transaction(func(tx *gorm.DB) error {
	if err := tx.Create(&user).Error; err != nil {
		return err
	}
	if err := tx.Create(&profile).Error; err != nil {
		return err
	}
	return nil
})

钩子(Hooks)

钩子允许你在特定事件发生前后执行自定义逻辑:

type User struct {
	ID   uint
	Name string
	Slug string
}

// 创建前生成 slug
func (u *User) BeforeCreate(tx *gorm.DB) error {
	u.Slug = strings.ToLower(strings.ReplaceAll(u.Name, " ", "-"))
	return nil
}

// 创建后
func (u *User) AfterCreate(tx *gorm.DB) error {
	log.Printf("用户 %s 创建成功", u.Name)
	return nil
}

// 更新前
func (u *User) BeforeUpdate(tx *gorm.DB) error {
	// 检查权限等
	return nil
}

// 删除前
func (u *User) BeforeDelete(tx *gorm.DB) error {
	// 清理关联数据
	return nil
}

Scopes(作用域)

Scopes 允许你定义可复用的查询条件:

func ActiveUsers(db *gorm.DB) *gorm.DB {
	return db.Where("is_active = ?", true)
}

func OlderThan(age int) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		return db.Where("age > ?", age)
	}
}

func OrderBy(field string) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		return db.Order(field)
	}
}

// 使用
db.Scopes(ActiveUsers, OlderThan(20), OrderBy("age desc")).Find(&users)

分页

type Pagination struct {
	Page     int
	PageSize int
	Total    int64
}

func Paginate(p *Pagination) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		if p.Page <= 0 {
			p.Page = 1
		}
		if p.PageSize <= 0 {
			p.PageSize = 10
		}
		if p.PageSize > 100 {
			p.PageSize = 100
		}
		
		offset := (p.Page - 1) * p.PageSize
		return db.Offset(offset).Limit(p.PageSize)
	}
}

// 使用
p := &Pagination{Page: 1, PageSize: 20}
db.Scopes(Paginate(p)).Find(&users)
db.Model(&User{}).Count(&p.Total)

fmt.Printf("共 %d 条记录,当前第 %d 页\n", p.Total, p.Page)

性能优化

1. 连接池配置

sqlDB, _ := db.DB()

sqlDB.SetMaxOpenConns(100)
sqlDB.SetMaxIdleConns(10)
sqlDB.SetConnMaxLifetime(time.Hour)

2. 使用索引

// 创建索引
db.Migrator().CreateIndex(&User{}, "Email")
db.Migrator().CreateIndex(&User{}, "idx_age_active")

3. 批量操作

// 批量插入
db.CreateInBatches(&users, 100)

// 批量更新
db.Model(&User{}).Where("age < ?", 20).Updates(map[string]interface{}{
	"is_active": false,
})

4. 避免 N+1 查询

// ❌ N+1 查询
var users []User
db.Find(&users)
for _, user := range users {
	db.Model(&user).Association("Posts").Find(&posts)  // 每个用户都查一次
}

// ✅ 预加载
db.Preload("Posts").Find(&users)

实战:博客系统

让我们用 GORM 实现一个简单的博客系统:

package main

import (
	"fmt"
	"log"
	"time"
	
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

type User struct {
	ID        uint   `gorm:"primarykey"`
	Name      string `gorm:"not null"`
	Email     string `gorm:"uniqueIndex;not null"`
	Posts     []Post
	CreatedAt time.Time
}

type Post struct {
	ID        uint   `gorm:"primarykey"`
	UserID    uint
	User      User
	Title     string `gorm:"not null"`
	Content   string `gorm:"type:text"`
	Tags      []Tag  `gorm:"many2many:post_tags;"`
	CreatedAt time.Time
	UpdatedAt time.Time
}

type Tag struct {
	ID    uint   `gorm:"primarykey"`
	Name  string `gorm:"uniqueIndex;not null"`
	Posts []Post `gorm:"many2many:post_tags;"`
}

func main() {
	db, err := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}
	
	db.AutoMigrate(&User{}, &Post{}, &Tag{})
	
	// 创建用户
	user := User{Name: "张三", Email: "zhangsan@example.com"}
	db.Create(&user)
	
	// 创建标签
	tag1 := Tag{Name: "Go"}
	tag2 := Tag{Name: "教程"}
	db.Create(&tag1)
	db.Create(&tag2)
	
	// 创建文章
	post := Post{
		UserID:  user.ID,
		Title:   "GORM 入门",
		Content: "这是一篇关于 GORM 的教程...",
		Tags:    []Tag{tag1, tag2},
	}
	db.Create(&post)
	
	// 查询用户及其文章和标签
	var userWithPosts User
	db.Preload("Posts.Tags").First(&userWithPosts, user.ID)
	
	fmt.Printf("用户: %s\n", userWithPosts.Name)
	for _, p := range userWithPosts.Posts {
		fmt.Printf("  文章: %s\n", p.Title)
		for _, t := range p.Tags {
			fmt.Printf("    标签: %s\n", t.Name)
		}
	}
	
	// 统计每个用户的文章数
type UserPostCount struct {
	Name      string
	PostCount int64
}
var counts []UserPostCount
db.Model(&User{}).
	Select("users.name, count(posts.id) as post_count").
	Joins("left join posts on posts.user_id = users.id").
	Group("users.id").
	Scan(&counts)

	for _, c := range counts {
		fmt.Printf("%s: %d 篇文章\n", c.Name, c.PostCount)
	}
}

小结

今天我们学习了 GORM:

  1. 基础操作:连接、模型定义、自动迁移
  2. CRUD:增删改查的各种用法
  3. 关联关系:一对一、一对多、多对多
  4. 事务:手动事务和闭包事务
  5. 钩子:生命周期回调
  6. Scopes:可复用的查询条件
  7. 性能优化:连接池、索引、预加载
  8. 实战:博客系统

GORM 极大地简化了数据库操作,让你能专注于业务逻辑。但在复杂查询场景下,仍然可以结合原生 SQL 使用。

练习时间

  1. 电商系统:实现用户、商品、订单、购物车的模型和关联
  2. 评论系统:支持嵌套评论(树形结构)
  3. 权限系统:实现 RBAC(基于角色的访问控制)
  4. 全文搜索:结合 MySQL 的全文索引实现文章搜索

我们下篇见!👋


参考资料:

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页