🚀 我写了一个让 GORM 查询不再写字符串的库:gorm-query

26 阅读3分钟

🚀 我写了一个让 GORM 查询不再写字符串的库:gorm-query

在 Go 生态中,GORM 是最流行的 ORM 之一。

但在中大型项目里,我们经常会遇到一些非常典型的问题:

  • 查询条件大量使用字符串
  • Repository 方法爆炸
  • 事务逻辑污染业务代码

为了解决这些问题,我写了一个开源库:

👉 gorm-query
github.com/im-wmkong/g…

它为 GORM 提供了一套组合方案:

强类型查询 + Query Builder + 泛型 Repository + Context 事务

💥 一次线上 Bug,让我重新思考 GORM 查询

几个月前,我们线上出现了一个非常隐蔽的问题。

某个接口突然报错:

Unknown column 'user_name'

排查后发现代码是这样的:

db.Where("user_name = ?", name)

但数据库字段其实已经改成:

username

于是事情变成了这样:

  • 代码 ✅ 编译正常
  • CI ✅ 测试正常
  • 代码 ✅ 成功上线
  • 用户 ❌ 访问接口直接报错

原因其实很简单:

GORM 查询字段是字符串,IDE 和编译器都无法检查。

这让我重新思考一个问题:

为什么 Go ORM 查询还在大量使用字符串?

🤕 GORM 在中大型项目里的三个痛点

1️⃣ 魔法字符串

最常见的代码:

db.Where("age >= ?", 18).
   Where("user_name LIKE ?", "%wmkong%")

问题很明显:

  • 字段写错 ❌ 编译不报错
  • IDE ❌ 没有自动补全
  • 重构 ❌ 无法自动修改

2️⃣ Repository 方法爆炸

很多项目会写 Repository:

type UserRepository interface {
    FindByName(ctx context.Context, name string) ([]*User, error)
    FindByAge(ctx context.Context, age int) ([]*User, error)
    FindByStatus(ctx context.Context, status int) ([]*User, error)
}

随着业务增长,很快就会变成:

FindByNameAndStatus
FindByAgeAndStatus
FindByEmailAndStatus
FindByStatusWithPage

Repository 很容易膨胀到几十个方法。

3️⃣ 事务污染业务层

很多代码会这样写:

func (s *UserService) CreateUser(tx *gorm.DB, user *User)

结果就是:

  • *gorm.DB 在项目中到处传递
  • Service 层和 ORM 强耦合

💡 gorm-query 的解决方案

gorm-query 的核心思路其实很简单:

强类型字段 + Query Builder + 泛型 Repository + Context 事务

目标是让查询代码变成这样:

query.New().
    Where(
        UserProps.Age.Gte(18),
        UserProps.UserName.Contains("wmkong"),
    ).
    Order(UserProps.ID.Desc())

而不是这样:

db.Where("age >= ?", 18)

🧩 强类型字段:告别字符串查询

gorm-query 通过 代码生成 创建字段对象。

Model 示例:

//go:generate go run github.com/im-wmkong/gorm-query/cmd/gen-props@latest -type=User
type User struct {
    gorm.Model
    UserName string `gorm:"column:user_name"`
    Email    string
    Age      int
    Status   int
}

执行代码生成:

go generate ./...

生成代码示例:

type userProps struct {
    UserName query.Column
    Email    query.Column
    Age      query.Column
    Status   query.Column
}

var UserProps = userProps{
    UserName: "user_name",
    Email:    "email",
    Age:      "age",
    Status:   "status",
}

于是查询可以写成:

UserProps.UserName
UserProps.Age

IDE 可以自动补全字段名 👍

🧱 Query Builder:灵活组合查询

Query Builder 用来构建查询条件:

qb := query.New().
    Where(
        UserProps.Age.Gte(18),
        UserProps.Status.Eq(1),
    ).
    Order(UserProps.ID.Desc()).
    Page(1, 20)

最终应用到 GORM:

err := qb.Apply(db.Model(&User{})).Find(&users).Error

相比字符串查询:

db.Where("age >= ?", 18)

Builder 方式更加:

  • 类型安全
  • 可组合
  • 易复用

🏗 BaseRepository:让 Repository 回归极简

在传统架构中,Repository 同时承担两件事情:

数据访问 + 查询构建

而在 gorm-query 中,这两个职责被拆开:

Query Builder 负责构建查询
Repository 负责执行查询

gorm-query 提供了一个泛型仓储:

repo.BaseRepository[T]

它内置常见操作:

  • Create
  • Update
  • Delete
  • Find
  • First
  • Count

定义 Repository 非常简单:

type UserRepository struct {
    repo.BaseRepository[model.User]
}

func NewUserRepository(dbClient db.Client) *UserRepository {
    return &UserRepository{
        repo.New[model.User](dbClient),
    }
}

这样就已经拥有完整 CRUD 能力。

🔗 Builder + Repository

当 Query Builder 和 BaseRepository 结合后:

qb := query.New().
    Where(UserProps.Status.Eq(1)).
    Where(UserProps.Age.Gte(18))

users, err := userRepo.Find(ctx, qb)

执行流程如下:

Query Builder
      ↓
BaseRepository.Find
      ↓
qb.Apply(db)
      ↓
GORM 执行 SQL

🔄 Context 事务管理

gorm-query 通过 Context 传递事务。

初始化:

dbClient := db.NewClient(gormDB)

在 Service 中:

func (s *UserService) CreateUserAndProfile(ctx context.Context, user *User, profile *Profile) error {
    return s.tm.Transaction(ctx, func(txCtx context.Context) error {

        if err := s.userRepo.Create(txCtx, user); err != nil {
            return err
        }

        profile.UserID = user.ID

        if err := s.profileRepo.Create(txCtx, profile); err != nil {
            return err
        }

        return nil
    })
}

Service 层 完全不需要接触 *gorm.DB

🧪 动态查询示例

func (s *UserService) GetUsers(ctx context.Context, name string, minAge int) ([]*User, error) {

    qb := query.New().
        Where(UserProps.Status.Eq(1))

    if name != "" {
        qb = qb.Where(UserProps.UserName.Contains(name))
    }

    if minAge > 0 {
        qb = qb.Where(UserProps.Age.Gte(minAge))
    }

    return s.userRepo.Find(ctx, qb)
}

Repository 无需新增任何方法

🧭 项目架构

整体架构如下:

Service Layer
      │
      ▼
Query Builder
      │
      ▼
Repository (BaseRepository[T])
      │
      ▼
gorm-query
      │
      ▼
GORM
      │
      ▼
Database

核心思想:

Builder + Repository + Context Transaction

⭐ 项目地址

github.com/im-wmkong/g…

如果你在使用 GORM,并希望拥有:

  • 强类型查询
  • 灵活的查询组合
  • 极简 Repository
  • 优雅事务管理

欢迎试试 gorm-query,也欢迎 Star ⭐。