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:
- 基础操作:连接、模型定义、自动迁移
- CRUD:增删改查的各种用法
- 关联关系:一对一、一对多、多对多
- 事务:手动事务和闭包事务
- 钩子:生命周期回调
- Scopes:可复用的查询条件
- 性能优化:连接池、索引、预加载
- 实战:博客系统
GORM 极大地简化了数据库操作,让你能专注于业务逻辑。但在复杂查询场景下,仍然可以结合原生 SQL 使用。
练习时间
- 电商系统:实现用户、商品、订单、购物车的模型和关联
- 评论系统:支持嵌套评论(树形结构)
- 权限系统:实现 RBAC(基于角色的访问控制)
- 全文搜索:结合 MySQL 的全文索引实现文章搜索
我们下篇见!👋
参考资料:
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。