Appearance
01-GORM 简介与模型定义
GORM 是 Go 语言中最流行的 ORM(对象关系映射)库,它将结构体与数据库表建立映射关系,让开发者可以用 Go 代码操作数据库而无需手写大量 SQL。本篇将从安装初始化讲起,深入到模型定义、字段标签、模型约定与 AutoMigrate,最终通过一个完整的博客系统模型示例把所有知识点串起来。
GORM 简介
GORM 是一个开发者友好的 Go 语言 ORM 库,自 2013 年发布以来已经成为 Go 生态中事实上的 ORM 标准。它的主要特性包括:
- 全功能 ORM:支持增删改查、关联关系、预加载、事务等
- 链式 API:可复用的 Scopes、灵活的查询构造器
- 钩子(Hook)机制:BeforeCreate、AfterUpdate 等生命周期回调
- 自动迁移:AutoMigrate 自动同步表结构
- 多数据库驱动:MySQL、PostgreSQL、SQLite、SQL Server
- 高级特性:软删除、复合主键、乐观锁、多态关联
- 插件生态:分页、读写分离、Prometheus 监控、OpenTracing
官方网站:https://gorm.io
GORM 的设计哲学是"开发者友好"——它隐藏了大部分样板代码,让你专注于业务逻辑;同时又足够灵活,遇到复杂场景可以随时回退到原生 SQL。
安装与初始化
安装 GORM
GORM 的核心库是 gorm.io/gorm,它本身不包含任何数据库驱动,需要根据实际使用的数据库安装对应的驱动包。
bash
# 安装 GORM 核心
go get -u gorm.io/gorm
# 安装 MySQL 驱动
go get -u gorm.io/driver/mysql
# 安装 PostgreSQL 驱动
go get -u gorm.io/driver/postgres
# 安装 SQLite 驱动(开发调试很方便)
go get -u gorm.io/driver/sqlite初始化项目
创建一个新项目并初始化 go.mod:
bash
mkdir gorm-demo
cd gorm-demo
go mod init gorm-demo
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql连接数据库
连接 MySQL
MySQL 是最常用的关系型数据库,下面演示如何用 GORM 连接 MySQL:
go
package main
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func InitMySQL() (*gorm.DB, error) {
// DSN 格式:用户名:密码@tcp(主机:端口)/数据库名?参数
dsn := "root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Warn,
IgnoreRecordNotFoundError: true,
Colorful: true,
},
),
})
if err != nil {
return nil, fmt.Errorf("连接 MySQL 失败: %w", err)
}
return db, nil
}
func main() {
db, err := InitMySQL()
if err != nil {
log.Fatal(err)
}
fmt.Println("MySQL 连接成功:", db)
}DSN 各参数含义:
charset=utf8mb4:字符集,支持 emoji 等 4 字节字符parseTime=True:自动解析 MySQL 时间类型为 Go 的time.Timeloc=Local:使用本地时区
连接 PostgreSQL
PostgreSQL 在复杂业务场景下也很常见,连接方式类似:
go
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func InitPostgres() (*gorm.DB, error) {
dsn := "host=127.0.0.1 user=postgres password=123456 dbname=demo port=5432 sslmode=disable TimeZone=Asia/Shanghai"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("连接 PostgreSQL 失败: %w", err)
}
return db, nil
}
func main() {
db, err := InitPostgres()
if err != nil {
log.Fatal(err)
}
// 简单 ping 一下
sqlDB, _ := db.DB()
_ = sqlDB.Ping()
fmt.Println("PostgreSQL 连接成功,时间:", time.Now())
}连接 SQLite
SQLite 是单文件数据库,非常适合开发、测试、单机工具:
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func InitSQLite() (*gorm.DB, error) {
// 使用文件数据库,也可以使用 ":memory:" 创建内存数据库
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("连接 SQLite 失败: %w", err)
}
return db, nil
}
func main() {
db, err := InitSQLite()
if err != nil {
log.Fatal(err)
}
fmt.Println("SQLite 连接成功:", db.Name())
}连接池配置
无论使用哪种数据库,生产环境都必须配置连接池,否则容易出现连接耗尽或连接失效等问题。GORM 通过 db.DB() 返回底层的 *sql.DB,再调用其方法配置连接池。
go
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func InitDBWithPool() (*gorm.DB, error) {
dsn := "root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
// 最大空闲连接数:建议设置为最大连接数的 1/3 到 1/2
sqlDB.SetMaxIdleConns(10)
// 最大打开连接数:根据数据库服务器配置和并发量设置
// 注意:不要超过数据库服务器 max_connections 配置
sqlDB.SetMaxOpenConns(100)
// 连接最大存活时间:建议小于数据库 wait_timeout
// MySQL 默认 wait_timeout 是 8 小时,这里设为 1 小时
sqlDB.SetConnMaxLifetime(time.Hour)
// 连接最大空闲时间:超过这个时间的空闲连接会被关闭
// 建议设置为几分钟,避免使用过期连接
sqlDB.SetConnMaxIdleTime(10 * time.Minute)
return db, nil
}
func main() {
db, err := InitDBWithPool()
if err != nil {
log.Fatal(err)
}
sqlDB, _ := db.DB()
stats := sqlDB.Stats()
fmt.Printf("最大空闲连接: %d, 最大打开连接: %d\n",
stats.MaxIdleConnections, stats.MaxOpenConnections)
}连接池参数调优建议:
MaxIdleConns:太小会导致频繁建立连接,太大会占用资源,建议 10-20MaxOpenConns:根据数据库性能和并发量设置,常见 100-500ConnMaxLifetime:必须小于数据库wait_timeout,避免连接失效ConnMaxIdleTime:保持空闲连接新鲜,避免被数据库单方面关闭
模型定义基础
GORM 使用结构体(struct)映射数据库表,结构体字段映射表列。默认情况下,GORM 将结构体名转换为蛇形(snake_case)作为表名,字段名转换为蛇形作为列名。
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// User 结构体映射 users 表
type User struct {
ID uint // 默认作为主键
Name string // 映射到 name 列
Email string // 映射到 email 列
Age int // 映射到 age 列
CreatedAt string // 映射到 created_at 列(非内置类型)
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 自动迁移:根据 User 结构体创建 users 表
if err := db.AutoMigrate(&User{}); err != nil {
log.Fatal(err)
}
// 插入一条记录
user := User{Name: "张三", Email: "zhangsan@example.com", Age: 25}
result := db.Create(&user)
fmt.Printf("插入 %d 行, ID: %d\n", result.RowsAffected, user.ID)
}字段标签
通过结构体 tag 可以精细控制字段的映射规则、类型、约束等。GORM 的 tag 以 gorm: 开头,多个配置用 ; 分隔。
go
package main
import (
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Product struct {
// column 指定列名;primaryKey 指定主键
ID uint `gorm:"column:id;primaryKey"`
// type 指定列类型;size 指定长度;not null 非空约束
Name string `gorm:"column:name;type:varchar(100);size:100;not null"`
// default 指定默认值;comment 添加注释
Status int `gorm:"column:status;default:1;comment:状态:1启用 0禁用"`
// unique 唯一约束;index 创建索引
Email string `gorm:"column:email;type:varchar(150);uniqueIndex"`
// autoCreateTime 自动记录创建时间(秒)
CreatedAt int64 `gorm:"column:created_at;autoCreateTime"`
// autoUpdateTime 自动记录更新时间(秒)
UpdatedAt int64 `gorm:"column:updated_at;autoUpdateTime"`
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&Product{})
}常用字段标签汇总:
| 标签 | 说明 | 示例 |
|---|---|---|
column | 列名 | gorm:"column:user_name" |
type | 列类型 | gorm:"type:varchar(100)" |
size | 字段长度 | gorm:"size:255" |
primaryKey | 主键 | gorm:"primaryKey" |
autoIncrement | 自增 | gorm:"autoIncrement" |
not null | 非空 | gorm:"not null" |
default | 默认值 | gorm:"default:0" |
unique | 唯一约束 | gorm:"unique" |
index | 普通索引 | gorm:"index" |
uniqueIndex | 唯一索引 | gorm:"uniqueIndex" |
comment | 注释 | gorm:"comment:用户名" |
autoCreateTime | 自动创建时间 | gorm:"autoCreateTime" |
autoUpdateTime | 自动更新时间 | gorm:"autoUpdateTime" |
模型约定
GORM 有一些默认约定,能减少大量样板代码:
- ID 字段默认作为主键
- CreatedAt 自动记录记录创建时间
- UpdatedAt 自动记录记录更新时间
- DeletedAt 软删除字段(
gorm.DeletedAt类型)
go
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// BaseModel 内嵌基础模型,复用约定字段
type BaseModel struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time // 自动创建时间
UpdatedAt time.Time // 自动更新时间
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除字段
}
type Article struct {
BaseModel // 内嵌,继承 ID、CreatedAt、UpdatedAt、DeletedAt
Title string
Content string
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&Article{})
// 创建:CreatedAt 和 UpdatedAt 自动填充
article := Article{Title: "Hello GORM", Content: "GORM 模型约定示例"}
db.Create(&article)
fmt.Printf("创建时间: %s, 更新时间: %s\n", article.CreatedAt, article.UpdatedAt)
// 更新:UpdatedAt 自动更新
article.Title = "Hello GORM Updated"
db.Save(&article)
fmt.Printf("更新后 UpdatedAt: %s\n", article.UpdatedAt)
// 软删除:Delete 不会真正删除,而是设置 DeletedAt
db.Delete(&article)
// 普通查询找不到已软删除的记录
var count int64
db.Model(&Article{}).Count(&count)
fmt.Printf("普通查询数量: %d\n", count)
// 使用 Unscoped 可以查询到软删除的记录
var allArticles []Article
db.Unscoped().Find(&allArticles)
fmt.Printf("包含软删除的查询数量: %d\n", len(allArticles))
}软删除的优势:
- 误删数据可以恢复
- 满足审计合规要求
- 避免外键级联删除导致的连锁问题
自定义表名
如果默认的蛇形命名不满足需求,可以实现 Tabler 接口自定义表名:
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Person struct {
ID uint
Name string
}
// TableName 自定义表名,实现 Tabler 接口
func (Person) TableName() string {
return "t_person" // 自定义表名前缀
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 禁用默认表名复数形式(默认 persons 会变成 people)
// 全局禁用复数:
// db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{
// NamingStrategy: schema.NamingStrategy{SingularTable: true},
// })
db.AutoMigrate(&Person{})
p := Person{Name: "Alice"}
db.Create(&p)
var found Person
db.First(&found, p.ID)
fmt.Println("查询到:", found.Name)
}也可以临时指定表名:
go
// 查询时临时指定表名
db.Table("custom_users").Find(&users)AutoMigrate 详解与注意事项
AutoMigrate 会根据模型结构创建表、缺失的列、缺失的索引。但它有一些重要限制需要注意:
AutoMigrate 会做的事:
- 创建表(如果不存在)
- 添加缺失的列
- 添加缺失的索引
- 修改列类型(部分数据库支持)
AutoMigrate 不会做的事:
- 不会删除列(避免数据丢失)
- 不会删除索引
- 不会重命名列
- 不会修改列的约束(如 NOT NULL)
go
package main
import (
"fmt"
"log"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
type Book struct {
ID uint `gorm:"primaryKey"`
Title string `gorm:"type:varchar(200);not null"`
Author string `gorm:"type:varchar(100)"`
Price float64
ISBN string `gorm:"uniqueIndex"`
}
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// AutoMigrate 会创建 books 表
if err := db.AutoMigrate(&Book{}); err != nil {
log.Fatal("迁移失败:", err)
}
// 验证表是否创建
migrator := db.Migrator()
if migrator.HasTable(&Book{}) {
fmt.Println("books 表已创建")
}
if migrator.HasColumn(&Book{}, "ISBN") {
fmt.Println("isbn 列已创建")
}
if migrator.HasIndex(&Book{}, "ISBN") {
fmt.Println("isbn 唯一索引已创建")
}
// 后续如果给 Book 增加新字段 Description
// 再次 AutoMigrate 会自动添加该列
}AutoMigrate 的注意事项:
- 生产环境慎用:AutoMigrate 适合开发期快速迭代,生产环境建议使用专业的迁移工具(如 golang-migrate)以获得版本控制和回滚能力
- 不会删列:删除模型字段后再次 AutoMigrate 不会删除数据库列,需要手动处理
- 类型变更有限:某些类型变更可能失败,需要手动 SQL
- 避免重复迁移:程序启动时重复 AutoMigrate 同一个表是低效的,建议有条件地执行
完整示例:博客系统模型定义
下面通过一个博客系统的模型定义综合运用以上知识,包含 User、Post、Comment 三个模型及其关联关系。
go
package main
import (
"fmt"
"log"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// BaseModel 公共字段
type BaseModel struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// User 用户模型
type User struct {
BaseModel
Username string `gorm:"type:varchar(50);uniqueIndex;not null" json:"username"`
Email string `gorm:"type:varchar(150);uniqueIndex;not null" json:"email"`
Password string `gorm:"type:varchar(100);not null" json:"-"` // json:"-" 不序列化
Nickname string `gorm:"type:varchar(50)" json:"nickname"`
Age int `gorm:"default:0" json:"age"`
Status int `gorm:"default:1;comment:状态 1启用 0禁用" json:"status"`
// 一对多:一个用户有多篇文章
Posts []Post `gorm:"foreignKey:UserID" json:"posts,omitempty"`
}
// Post 文章模型
type Post struct {
BaseModel
Title string `gorm:"type:varchar(200);not null;index" json:"title"`
Content string `gorm:"type:text" json:"content"`
ViewCount int `gorm:"default:0" json:"view_count"`
// 多对一:文章属于某个用户
UserID uint `gorm:"index;not null" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
// 一对多:一篇文章有多条评论
Comments []Comment `gorm:"foreignKey:PostID" json:"comments,omitempty"`
}
// Comment 评论模型
type Comment struct {
BaseModel
Content string `gorm:"type:text;not null" json:"content"`
PostID uint `gorm:"index;not null" json:"post_id"`
Post Post `gorm:"foreignKey:PostID" json:"-"`
UserID uint `gorm:"index;not null" json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
}
func main() {
db, err := gorm.Open(sqlite.Open("blog.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 自动迁移所有模型(注意顺序,先迁移被引用的表)
if err := db.AutoMigrate(&User{}, &Post{}, &Comment{}); err != nil {
log.Fatal("迁移失败:", err)
}
// 创建用户
user := User{
Username: "alice",
Email: "alice@example.com",
Password: "hashed_password",
Nickname: "爱丽丝",
Age: 28,
}
if err := db.Create(&user).Error; err != nil {
log.Fatal(err)
}
fmt.Printf("用户创建成功,ID: %d\n", user.ID)
// 创建文章
post := Post{
Title: "GORM 入门指南",
Content: "GORM 是 Go 最流行的 ORM...",
UserID: user.ID,
}
if err := db.Create(&post).Error; err != nil {
log.Fatal(err)
}
fmt.Printf("文章创建成功,ID: %d\n", post.ID)
// 创建评论
comment := Comment{
Content: "写得很好!",
PostID: post.ID,
UserID: user.ID,
}
if err := db.Create(&comment).Error; err != nil {
log.Fatal(err)
}
fmt.Printf("评论创建成功,ID: %d\n", comment.ID)
// 预加载查询,演示关联
var fullPost Post
if err := db.Preload("User").Preload("Comments").First(&fullPost, post.ID).Error; err != nil {
log.Fatal(err)
}
fmt.Printf("文章: %s, 作者: %s, 评论数: %d\n",
fullPost.Title, fullPost.User.Nickname, len(fullPost.Comments))
// 软删除演示
db.Delete(&comment)
var activeComments []Comment
db.Where("post_id = ?", post.ID).Find(&activeComments)
fmt.Printf("软删除后剩余评论数: %d\n", len(activeComments))
}小结
本篇系统介绍了 GORM 的核心入门知识:
- GORM 是什么:Go 最流行的全功能 ORM,开发者友好且生态完善
- 安装与初始化:核心库
gorm.io/gorm加上对应数据库驱动 - 多数据库连接:MySQL、PostgreSQL、SQLite 各有 DSN 格式
- 连接池配置:通过
db.DB()获取底层*sql.DB配置四个关键参数 - 模型定义:结构体映射表,字段映射列,默认蛇形命名
- 字段标签:
gorm:tag 控制类型、约束、索引、注释等 - 模型约定:ID、CreatedAt、UpdatedAt、DeletedAt 四个内置字段
- 自定义表名:实现
Tabler接口的TableName()方法 - AutoMigrate:自动建表加列,但不会删列、不会重命名,生产慎用
- 综合示例:博客系统 User-Post-Comment 三表模型与关联
下一篇我们将深入 CRUD 操作与查询基础,掌握 GORM 的核心数据操作能力。