Appearance
07-数据库集成:GORM实战
数据库是几乎所有 Web 应用的核心组件。在 Go 生态中,GORM 是最受欢迎的 ORM 之一,它提供了全功能的 ORM 能力,同时保持了良好的开发体验。本篇将系统讲解 GORM 的使用,以及如何在 Gin 中以分层架构集成数据库。
GORM 简介
GORM 是一个开发者友好的 Go 语言 ORM 库,主要特性包括:
- 全功能 ORM(增删改查、关联、预加载)
- 链式 API、Scopes 复用查询
- 自动迁移(AutoMigrate)
- 钩子(Hook)机制:BeforeCreate、AfterUpdate 等
- 事务支持
- 多数据库驱动:MySQL、PostgreSQL、SQLite、SQL Server
- 复合主键、软删除、乐观锁
- 插件生态:分页、读写分离、Prometheus 监控
官方网站:https://gorm.io
安装与初始化:连接 MySQL、配置连接池
安装
bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql如果用 SQLite(开发调试很方便):
bash
go get -u gorm.io/driver/sqlite连接 MySQL 并配置连接池
go
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"time"
)
func InitDB() (*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{
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("连接数据库失败: %w", err)
}
// 获取底层 *sql.DB 配置连接池
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(10) // 最大空闲连接
sqlDB.SetMaxOpenConns(100) // 最大打开连接
sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大存活时间
return db, nil
}
func main() {
db, err := InitDB()
if err != nil {
log.Fatal(err)
}
fmt.Println("数据库连接成功:", db)
}DSN 各字段含义
text
用户名:密码@tcp(主机:端口)/数据库名?charset=utf8mb4&parseTime=True&loc=Localcharset=utf8mb4:支持完整 Unicode,包括 emojiparseTime=True:把 MySQL 的DATETIME自动解析为 Go 的time.Timeloc=Local:使用本地时区
定义模型:结构体与表映射
GORM 用结构体映射数据表,通过结构体标签(struct tag)配置映射关系。
go
package main
import (
"gorm.io/gorm"
"time"
)
// User 用户表
type User struct {
ID uint `gorm:"primaryKey"` // 主键
Username string `gorm:"size:50;uniqueIndex;not null"` // 长度50、唯一索引、非空
Email string `gorm:"size:100;uniqueIndex"`
Password string `gorm:"size:100;not null"`
Age int `gorm:"default:0"`
Birthday *time.Time
IsActive bool `gorm:"default:true"`
CreatedAt time.Time // GORM 自动管理
UpdatedAt time.Time // GORM 自动管理
DeletedAt gorm.DeletedAt `gorm:"index"` // 软删除字段
}
// TableName 自定义表名(默认是 user 的复数 users)
func (User) TableName() string {
return "users"
}常用标签
| 标签 | 说明 |
|---|---|
primaryKey | 主键 |
autoIncrement | 自增 |
size:n | 字段长度 |
not null | 非空 |
unique / uniqueIndex | 唯一 |
index | 普通索引 |
default:value | 默认值 |
column:xxx | 自定义列名 |
type:text | 自定义类型 |
- | 忽略字段 |
GORM 默认约定:
- 主键名为
ID - 表名为结构体名复数(
User→users) - 自动添加
CreatedAt、UpdatedAt - 字段
DeletedAt启用软删除
自动迁移:AutoMigrate
AutoMigrate 会根据模型创建表、添加缺失的列和索引,但不会删除列或修改列类型(为了安全)。
go
package main
import (
"fmt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"log"
)
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"size:50;uniqueIndex"`
Email string `gorm:"size:100"`
Age int
}
func main() {
// 用 SQLite 方便演示,无需装数据库
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
log.Fatal(err)
}
// 自动迁移
if err := db.AutoMigrate(&User{}); err != nil {
log.Fatal(err)
}
fmt.Println("迁移成功")
}生产环境建议用 SQL 迁移工具如
golang-migrate、Atlas、goose管理表结构变更,避免依赖 AutoMigrate。
CRUD 操作:Create、First、Find、Update、Delete
下面以 SQLite 为例演示完整 CRUD。
Create 创建
go
// 单条插入
user := User{Username: "alice", Email: "alice@x.com", Age: 20}
result := db.Create(&user)
fmt.Println(user.ID) // 插入后 ID 自动回填
fmt.Println(result.RowsAffected) // 影响行数
fmt.Println(result.Error) // 错误
// 批量插入
users := []User{
{Username: "bob", Email: "bob@x.com", Age: 25},
{Username: "carol", Email: "carol@x.com", Age: 30},
}
db.Create(&users)
// 指定字段创建
db.Select("Username", "Email").Create(&User{Username: "dave", Email: "dave@x.com", Age: 40})Read 查询
go
// 按主键
var u User
db.First(&u, 1) // SELECT * FROM users WHERE id=1
// 按条件
db.First(&u, "username = ?", "alice")
// 查询多条
var users []User
db.Find(&users) // 全表
db.Where("age > ?", 18).Find(&users) // 条件
db.Where("username IN ?", []string{"alice", "bob"}).Find(&users)
// 取一条
db.Where("email = ?", "alice@x.com").First(&u)
// 检查记录是否存在
var count int64
db.Model(&User{}).Where("username = ?", "alice").Count(&count)Update 更新
go
// 保存整个对象(注意零值不会更新)
u.Age = 21
db.Save(&u)
// 更新单列
db.Model(&User{}).Where("username = ?", "alice").Update("age", 22)
// 更新多列(结构体,零值不更新)
db.Model(&u).Updates(User{Age: 23, Email: "new@x.com"})
// 更新多列(map,零值也更新)
db.Model(&u).Updates(map[string]interface{}{"age": 0, "is_active": false})
// 表达式更新
db.Model(&User{}).Where("id = ?", 1).Update("age", gorm.Expr("age + ?", 1))Delete 删除
go
// 软删除(模型有 DeletedAt 字段)
db.Delete(&u) // DELETE ... WHERE id=u.id
db.Delete(&User{}, 1) // 按主键
db.Where("age < ?", 18).Delete(&User{}) // 条件删除
// 软删除后,普通查询找不到
db.Find(&users) // 不包含已删除的
// 查询包含已删除的
db.Unscoped().Find(&users)
// 永久删除
db.Unscoped().Delete(&u)条件查询:Where、Not、Or、Order、Limit、Offset
Where 三种写法
go
// 1. 字符串 + 占位符(推荐,防注入)
db.Where("age > ? AND age < ?", 18, 60).Find(&users)
// 2. 结构体(零值不参与)
db.Where(&User{Username: "alice", Age: 0}).Find(&users) // age 不参与
// 3. map
db.Where(map[string]interface{}{"username": "alice", "age": 20}).Find(&users)Not / Or
go
db.Not("username = ?", "alice").Find(&users)
db.Where("age = ?", 20).Or("age = ?", 25).Find(&users)
db.Where("username = ?", "alice").Or(User{Username: "bob"}).Find(&users)Order / Limit / Offset / Group / Having
go
db.Order("age desc, id asc").Find(&users)
db.Limit(10).Offset(20).Find(&users) // 分页
type Result struct {
Age int
Total int
}
var results []Result
db.Model(&User{}).
Select("age, count(*) as total").
Group("age").
Having("count(*) > ?", 1).
Find(&results)Distinct / Count
go
db.Distinct("age").Find(&users)
var total int64
db.Model(&User{}).Count(&total)Scopes 复用查询条件
go
func Paginate(page, size int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page <= 0 {
page = 1
}
if size <= 0 || size > 100 {
size = 10
}
offset := (page - 1) * size
return db.Offset(offset).Limit(size)
}
}
db.Scopes(Paginate(2, 10)).Find(&users)关联查询:一对一、一对多、多对多
一对一
go
type User struct {
ID uint
Name string
Profile Profile // 一对一
}
type Profile struct {
ID uint
UserID uint // 外键
Bio string
}查询时用 Preload 预加载关联:
go
db.Preload("Profile").First(&user, 1)
fmt.Println(user.Profile.Bio)一对多
go
type User struct {
ID uint
Name string
Orders []Order // 一对多
}
type Order struct {
ID uint
UserID uint
Amount float64
}go
db.Preload("Orders").First(&user, 1)
for _, o := range user.Orders {
fmt.Println(o.Amount)
}
// 带条件的预加载
db.Preload("Orders", "amount > ?", 100).First(&user, 1)多对多
go
type Tag struct {
ID uint
Name string
}
type Article struct {
ID uint
Title string
Tags []Tag `gorm:"many2many:article_tags;"` // 中间表
}go
// 创建时关联
db.Create(&Article{
Title: "GORM 入门",
Tags: []Tag{{Name: "Go"}, {Name: "DB"}},
})
// 查询
db.Preload("Tags").First(&article, 1)
// 追加关联
db.Model(&article).Association("Tags").Append(&Tag{Name: "ORM"})
// 替换关联
db.Model(&article).Association("Tags").Replace(&Tag{Name: "Database"})
// 删除关联(不删除记录本身)
db.Model(&article).Association("Tags").Delete(&tag)
// 清空关联
db.Model(&article).Association("Tags").Clear()Joins 关联查询
go
type Result struct {
Username string
OrderAmt float64
}
var results []Result
db.Table("users").
Select("users.username, orders.amount as order_amt").
Joins("LEFT JOIN orders ON orders.user_id = users.id").
Where("orders.amount > ?", 100).
Scan(&results)事务处理
GORM 提供三种事务使用方式。
方式一:自动事务(推荐)
go
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&User{Username: "alice"}).Error; err != nil {
return err // 返回 error 自动回滚
}
if err := tx.Create(&Order{UserID: 1, Amount: 99}).Error; err != nil {
return err
}
// 返回 nil 自动提交
return nil
})方式二:手动事务
go
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&order).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error方式三:嵌套事务(SavePoint)
go
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user2)
return errors.New("rollback inner") // 只回滚内层
})
tx.Create(&user3)
return nil // 外层正常提交
})在 Gin 中使用 GORM 的最佳实践
1. 封装数据库连接
把数据库初始化逻辑放在单独的包,便于复用:
go
// db/db.go
package db
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"log"
"os"
"time"
)
var DB *gorm.DB
func Init() {
dsn := os.Getenv("DB_DSN")
if dsn == "" {
dsn = "root:123456@tcp(127.0.0.1:3306)/demo?charset=utf8mb4&parseTime=True&loc=Local"
}
d, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("连接数据库失败:", err)
}
sqlDB, _ := d.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
DB = d
}2. 分层架构:Handler -> Service -> Repository
清晰分层让代码可维护、可测试:
- Handler(控制器层):解析 HTTP 请求、返回响应
- Service(业务层):核心业务逻辑
- Repository(数据访问层):数据库操作
下面以"用户管理"为例演示完整分层结构。
完整的 RESTful API 示例(用户管理,含数据库操作)
项目结构
text
user-api/
├── main.go
├── go.mod
├── db/
│ └── db.go
├── model/
│ └── user.go
├── repository/
│ └── user_repo.go
├── service/
│ └── user_service.go
└── handler/
└── user_handler.go为简化演示,下面把所有代码放在一个 main.go 文件里(用 SQLite):
go
package main
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"net/http"
"strconv"
"time"
)
// ============ 模型 ============
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:50;uniqueIndex;not null" json:"username"`
Email string `gorm:"size:100;uniqueIndex" json:"email"`
Age int `json:"age"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ============ Repository 层 ============
type UserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(u *User) error {
return r.db.Create(u).Error
}
func (r *UserRepository) FindByID(id uint) (*User, error) {
var u User
err := r.db.First(&u, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return &u, err
}
func (r *UserRepository) FindByUsername(username string) (*User, error) {
var u User
err := r.db.Where("username = ?", username).First(&u).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return &u, err
}
func (r *UserRepository) List(page, size int) ([]User, int64, error) {
var users []User
var total int64
r.db.Model(&User{}).Count(&total)
err := r.db.Offset((page - 1) * size).Limit(size).Find(&users).Error
return users, total, err
}
func (r *UserRepository) Update(u *User) error {
return r.db.Save(u).Error
}
func (r *UserRepository) Delete(id uint) error {
return r.db.Delete(&User{}, id).Error
}
// ============ Service 层 ============
type UserService struct {
repo *UserRepository
}
func NewUserService(repo *UserRepository) *UserService {
return &UserService{repo: repo}
}
func (s *UserService) Create(u *User) error {
// 业务校验:用户名不能重复
exist, err := s.repo.FindByUsername(u.Username)
if err != nil {
return err
}
if exist != nil {
return errors.New("用户名已存在")
}
if u.Age < 0 || u.Age > 150 {
return errors.New("年龄不合法")
}
return s.repo.Create(u)
}
func (s *UserService) Get(id uint) (*User, error) {
u, err := s.repo.FindByID(id)
if err != nil {
return nil, err
}
if u == nil {
return nil, errors.New("用户不存在")
}
return u, nil
}
func (s *UserService) List(page, size int) ([]User, int64, error) {
if page <= 0 {
page = 1
}
if size <= 0 || size > 100 {
size = 10
}
return s.repo.List(page, size)
}
func (s *UserService) Update(u *User) error {
exist, err := s.repo.FindByID(u.ID)
if err != nil {
return err
}
if exist == nil {
return errors.New("用户不存在")
}
return s.repo.Update(u)
}
func (s *UserService) Delete(id uint) error {
return s.repo.Delete(id)
}
// ============ Handler 层 ============
type UserHandler struct {
svc *UserService
}
func NewUserHandler(svc *UserService) *UserHandler {
return &UserHandler{svc: svc}
}
func (h *UserHandler) Register(r *gin.Engine) {
g := r.Group("/api/users")
{
g.POST("", h.Create)
g.GET("", h.List)
g.GET("/:id", h.Get)
g.PUT("/:id", h.Update)
g.DELETE("/:id", h.Delete)
}
}
func (h *UserHandler) Create(c *gin.Context) {
var u User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.svc.Create(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, u)
}
func (h *UserHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "10"))
users, total, err := h.svc.List(page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"data": users,
"total": total,
"page": page,
"size": size,
})
}
func (h *UserHandler) Get(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
u, err := h.svc.Get(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, u)
}
func (h *UserHandler) Update(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
var u User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u.ID = uint(id)
if err := h.svc.Update(&u); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, u)
}
func (h *UserHandler) Delete(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
return
}
if err := h.svc.Delete(uint(id)); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusNoContent, nil)
}
// ============ main ============
func main() {
// 初始化数据库
db, err := gorm.Open(sqlite.Open("users.db"), &gorm.Config{})
if err != nil {
panic(err)
}
db.AutoMigrate(&User{})
// 依赖注入
repo := NewUserRepository(db)
svc := NewUserService(repo)
handler := NewUserHandler(svc)
// 启动服务
r := gin.Default()
handler.Register(r)
fmt.Println("服务启动在 http://localhost:8080")
r.Run(":8080")
}测试 API
bash
# 创建用户
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"username":"alice","email":"alice@x.com","age":20}'
# 列表
curl "http://localhost:8080/api/users?page=1&size=10"
# 查询单个
curl http://localhost:8080/api/users/1
# 更新
curl -X PUT http://localhost:8080/api/users/1 \
-H "Content-Type: application/json" \
-d '{"username":"alice2","email":"alice2@x.com","age":21}'
# 删除
curl -X DELETE http://localhost:8080/api/users/1一些实战建议
- 数据库连接单例:使用全局
*gorm.DB或依赖注入,不要每个请求新建连接。 - 错误处理:把 GORM 错误转换为业务错误,避免暴露底层细节。
- 分页统一封装:用上面提到的
PaginateScope。 - N+1 查询陷阱:列表查询带关联时一定用
Preload,否则循环里查 N 次关联。 - 生产环境关闭 SQL 日志的 Colorful,并使用 JSON 格式便于日志收集。
- 索引优化:常用查询字段加索引,但不要无脑加,索引有写入开销。
- gorm.Expr 防注入:动态构建查询时务必用占位符
?。
小结
本篇系统讲解了 GORM 在 Gin 中的实战应用:
- 安装与初始化:连接 MySQL、配置连接池、日志级别。
- 模型定义:结构体映射表,标签配置字段,约定优于配置。
- 自动迁移:
AutoMigrate快速建表,生产环境建议用迁移工具。 - CRUD:Create / First / Find / Update / Delete 全套用法。
- 条件查询:Where / Not / Or / Order / Limit / Offset / Scopes。
- 关联查询:一对一、一对多、多对多,Preload 预加载防 N+1。
- 事务:自动事务(推荐)、手动事务、嵌套事务。
- 分层架构:Handler → Service → Repository,依赖注入。
- 综合示例:完整的用户管理 RESTful API。
下一篇我们将进入认证授权的世界,学习 JWT 与 Session 管理。