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

AI摘要
本文为技术知识分享,介绍了一个名为gorm-query的开源Go语言库,旨在解决GORM ORM在中大型项目中常见的痛点,如查询条件使用字符串易出错、Repository方法膨胀和事务逻辑污染业务代码。该库通过提供强类型字段、Query Builder、泛型Repository和基于Context的事务管理,提升代码的类型安全性、可维护性和架构清晰度。

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

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

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

  • 查询条件大量使用字符串

  • Repository 方法爆炸

  • 事务逻辑污染业务代码

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

👉 gorm-query

github.com/im-wmkong/gorm-query

它为 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/gorm-query

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

  • 强类型查询

  • 灵活的查询组合

  • 极简 Repository

  • 优雅事务管理

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

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 2
taadis

这个强类型不错 :+1:,楼主看看能不能完全独立出来,不依赖gorm?

3天前 评论
WmKong (楼主) 9小时前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!