GORM Query is a strongly-typed query builder and generic repository library built on top of GORM.
It eliminates fragile "magic strings" in GORM queries through code generation, providing a smooth fluent API experience. It also features enterprise-grade generic repositories and context-based transaction management.
- 🛡️ Strongly-typed Query Building: Say goodbye to
db.Where("age > ?", 18)and embraceUserProps.Age.Gte(18). Catch field name typos at compile time. - 📦 Out-of-the-box Generic Repository: Use
repo.BaseRepository[T]to gain full CRUD capabilities with a single line of code. - 🎯 Stop Bloating Repositories: Combine the universal query builder to compose dynamic queries on the fly—no more writing dozens of
FindByXxxmethods. - 🔄 Implicit Context Transactions: Pass transactions via
context.Context. Decouple your Service layer from the Repo layer without passing*gorm.DBeverywhere.
go get github.com/im-wmkong/gorm-queryDefine your GORM model as usual:
package model
import "gorm.io/gorm"
type User struct {
gorm.Model
UserName string `gorm:"column:user_name"`
Email string `gorm:"column:email"`
Age int `gorm:"column:age"`
Status int `gorm:"column:status"`
}Create a simple generation script (e.g., at cmd/gen/main.go) and pass the models you want to generate properties for:
package main
import (
"log"
"your_project_name/model" // Replace with your actual project path
"github.com/im-wmkong/gorm-query/genprops"
)
func main() {
// Initialize the generator and provide your models
err := genprops.New().GenerateAll([]any{
model.User{},
})
if err != nil {
log.Fatalf("generate failed: %v", err)
}
}Run the script from your terminal:
go run cmd/gen/main.goThis will automatically create a code file (e.g., props_gen.go) in your model directory containing the UserProps variable.
💡 Pro Tip: You can add
//go:generate go run cmd/gen/main.goto the top of any Go file and trigger generation usinggo generate ./...in your standard workflow.
Now you can use the generated UserProps with the Query Builder for type-safe queries:
import (
"your_project_name/model"
"github.com/im-wmkong/gorm-query/query"
)
// 1. Build queries fluently
qb := query.New().
Where(
model.UserProps.Age.Gte(18),
model.UserProps.UserName.Contains("wmkong"),
).
Page(1, 20).
Order(model.UserProps.ID.Desc())
// 2. Apply to gorm.DB
var users []model.User
err := qb.Apply(db).Find(&users).ErrorCombine db.Client and repo.BaseRepository to build a clean architecture:
Define Repositories and Service:
// Define UserRepository
type UserRepository struct {
repo.BaseRepository[model.User]
}
func NewUserRepository(dbClient db.Client) *UserRepository {
return &UserRepository{
repo.New[model.User](dbClient),
}
}
// Define ProfileRepository
type ProfileRepository struct {
repo.BaseRepository[model.Profile]
}
func NewProfileRepository(dbClient db.Client) *ProfileRepository {
return &ProfileRepository{
repo.New[model.Profile](dbClient),
}
}
// Define UserService
type UserService struct {
userRepo *UserRepository
profileRepo *ProfileRepository
tm db.TransactionManager
}
func NewUserService(userRepo *UserRepository, profileRepo *ProfileRepository, tm db.TransactionManager) *UserService {
return &UserService{
userRepo: userRepo,
profileRepo: profileRepo,
tm: tm,
}
}Initialization and Injection:
import (
"github.com/im-wmkong/gorm-query/db"
"github.com/im-wmkong/gorm-query/repo"
)
// 1. Initialize DB Client
dbClient := db.NewClient(gormDB)
// 2. Instantiate Repositories
userRepo := NewUserRepository(dbClient)
profileRepo := NewProfileRepository(dbClient)
// 3. Inject into Service
userService := NewUserService(userRepo, profileRepo, dbClient)Elegant Transaction Management in Service Layer:
// Business logic doesn't need to know about gorm.DB
func (s *UserService) CreateUserAndProfile(ctx context.Context, user *model.User, profile *model.Profile) error {
// Transaction starts here
return s.tm.Transaction(ctx, func(txCtx context.Context) error {
// Automatically uses the transaction stored in txCtx
if err := s.userRepo.Create(txCtx, user); err != nil {
return err
}
profile.UserID = user.ID
// If this fails, the previous Create will automatically roll back
if err := s.profileRepo.Create(txCtx, profile); err != nil {
return err
}
return nil
})
}Stop inflating your Repository interfaces with dozens of specific methods like FindByNameAndAge. Use the query.Builder to handle dynamic conditions in the Service layer while keeping your Repository clean.
func (s *UserService) GetUsers(ctx context.Context, name string, minAge int) ([]*model.User, error) {
// 1. Build dynamic conditions
qb := query.New().Where(model.UserProps.Status.Eq(1))
if name != "" {
qb = qb.Where(model.UserProps.UserName.Contains(name))
}
if minAge > 0 {
qb = qb.Where(model.UserProps.Age.Gte(minAge))
}
// 2. Pass the builder directly to the generic Find method
return s.userRepo.Find(ctx, qb)
}Use .Clone() to derive new queries from a base query without polluting the original:
baseQuery := query.New().Where(UserProps.Status.Eq(1))
// Derived Query A
adults := baseQuery.Clone().Where(UserProps.Age.Gte(18))
// Derived Query B (Will NOT include Age >= 18 condition)
minors := baseQuery.Clone().Where(UserProps.Age.Lt(18))Issues and Pull Requests are welcome!
Before submitting, please run:
make tidy
make generate
make testThis project is licensed under the MIT License.
