Skip to content

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=Local
  • charset=utf8mb4:支持完整 Unicode,包括 emoji
  • parseTime=True:把 MySQL 的 DATETIME 自动解析为 Go 的 time.Time
  • loc=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
  • 表名为结构体名复数(Userusers
  • 自动添加 CreatedAtUpdatedAt
  • 字段 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-migrateAtlasgoose 管理表结构变更,避免依赖 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

一些实战建议

  1. 数据库连接单例:使用全局 *gorm.DB 或依赖注入,不要每个请求新建连接。
  2. 错误处理:把 GORM 错误转换为业务错误,避免暴露底层细节。
  3. 分页统一封装:用上面提到的 Paginate Scope。
  4. N+1 查询陷阱:列表查询带关联时一定用 Preload,否则循环里查 N 次关联。
  5. 生产环境关闭 SQL 日志的 Colorful,并使用 JSON 格式便于日志收集。
  6. 索引优化:常用查询字段加索引,但不要无脑加,索引有写入开销。
  7. gorm.Expr 防注入:动态构建查询时务必用占位符 ?

小结

本篇系统讲解了 GORM 在 Gin 中的实战应用:

  1. 安装与初始化:连接 MySQL、配置连接池、日志级别。
  2. 模型定义:结构体映射表,标签配置字段,约定优于配置。
  3. 自动迁移AutoMigrate 快速建表,生产环境建议用迁移工具。
  4. CRUD:Create / First / Find / Update / Delete 全套用法。
  5. 条件查询:Where / Not / Or / Order / Limit / Offset / Scopes。
  6. 关联查询:一对一、一对多、多对多,Preload 预加载防 N+1。
  7. 事务:自动事务(推荐)、手动事务、嵌套事务。
  8. 分层架构:Handler → Service → Repository,依赖注入。
  9. 综合示例:完整的用户管理 RESTful API。

下一篇我们将进入认证授权的世界,学习 JWT 与 Session 管理。