A generic CRUD repository library for Go applications using GORM, providing type-safe database operations with transaction support, query scopes, and comprehensive security features.
- π Type-Safe Operations: Generic interfaces for compile-time type safety
- π Transaction Management: Context-aware transactions with nested support
- π Query Scopes: Reusable query builders for pagination, filtering, and ordering
- π‘οΈ Security First: SQL injection prevention and field name validation
- β‘ Performance: Optimized queries with preloading and batch operations
- π§ͺ Well Tested: 84.3% test coverage with comprehensive test suite
- π Clean API: Intuitive interfaces following Go best practices
go get github.com/itsLeonB/go-crudThe library is built around three core components:
package main
import (
"context"
"log"
"github.com/itsLeonB/go-crud"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// Define your model
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
Email string `gorm:"unique"`
Age int
CreatedAt time.Time
UpdatedAt time.Time
}
func main() {
// Initialize GORM
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
// Auto-migrate your models
db.AutoMigrate(&User{})
// Create repository and transactor
userRepo := crud.NewCRUDRepository[User](db)
transactor := crud.NewTransactor(db)
ctx := context.Background()
// Your application logic here...
}The CRUDRepository provides type-safe CRUD operations:
type CRUDRepository[T any] interface {
Insert(ctx context.Context, model T) (T, error)
FindAll(ctx context.Context, spec Specification[T]) ([]T, error)
FindFirst(ctx context.Context, spec Specification[T]) (T, error)
Update(ctx context.Context, model T) (T, error)
Delete(ctx context.Context, model T) error
BatchInsert(ctx context.Context, models []T) ([]T, error)
GetGormInstance(ctx context.Context) (*gorm.DB, error)
}Use specifications to build dynamic queries:
type Specification[T any] struct {
Model T // Model with fields set for WHERE conditions
PreloadRelations []string // Relations to eager load
ForUpdate bool // Whether to use SELECT ... FOR UPDATE
}ctx := context.Background()
userRepo := crud.NewCRUDRepository[User](db)
// Create a user
user := User{
Name: "John Doe",
Email: "john@example.com",
Age: 30,
}
createdUser, err := userRepo.Insert(ctx, user)
if err != nil {
log.Fatal("Failed to create user:", err)
}
// Find users
spec := crud.Specification[User]{
Model: User{Age: 30}, // Find users with age 30
}
users, err := userRepo.FindAll(ctx, spec)
if err != nil {
log.Fatal("Failed to find users:", err)
}
// Find first user
firstUser, err := userRepo.FindFirst(ctx, spec)
if err != nil {
log.Fatal("Failed to find user:", err)
}
// Update user
createdUser.Name = "John Smith"
updatedUser, err := userRepo.Update(ctx, createdUser)
if err != nil {
log.Fatal("Failed to update user:", err)
}
// Delete user
err = userRepo.Delete(ctx, updatedUser)
if err != nil {
log.Fatal("Failed to delete user:", err)
}// Batch insert multiple users
users := []User{
{Name: "Alice", Email: "alice@example.com", Age: 25},
{Name: "Bob", Email: "bob@example.com", Age: 30},
{Name: "Charlie", Email: "charlie@example.com", Age: 35},
}
createdUsers, err := userRepo.BatchInsert(ctx, users)
if err != nil {
log.Fatal("Failed to batch insert users:", err)
}// Find users with preloaded relations
spec := crud.Specification[User]{
Model: User{Age: 25},
PreloadRelations: []string{"Profile", "Posts"},
ForUpdate: false,
}
users, err := userRepo.FindAll(ctx, spec)
// Pessimistic locking
spec = crud.Specification[User]{
Model: User{ID: 1},
ForUpdate: true, // SELECT ... FOR UPDATE
}
user, err := userRepo.FindFirst(ctx, spec)transactor := crud.NewTransactor(db)
err := transactor.WithinTransaction(ctx, func(txCtx context.Context) error {
// All operations within this function use the same transaction
user := User{Name: "Alice", Email: "alice@example.com"}
createdUser, err := userRepo.Insert(txCtx, user)
if err != nil {
return err // Transaction will be rolled back
}
// Update in same transaction
createdUser.Age = 25
_, err = userRepo.Update(txCtx, createdUser)
if err != nil {
return err // Transaction will be rolled back
}
return nil // Transaction will be committed
})
if err != nil {
log.Fatal("Transaction failed:", err)
}// Begin transaction
txCtx, err := transactor.Begin(ctx)
if err != nil {
log.Fatal("Failed to begin transaction:", err)
}
// Perform operations
user, err := userRepo.Insert(txCtx, User{Name: "Bob"})
if err != nil {
transactor.Rollback(txCtx)
log.Fatal("Failed to insert user:", err)
}
// Commit transaction
err = transactor.Commit(txCtx)
if err != nil {
log.Fatal("Failed to commit transaction:", err)
}err := transactor.WithinTransaction(ctx, func(outerTxCtx context.Context) error {
// Outer transaction
user, err := userRepo.Insert(outerTxCtx, User{Name: "Outer"})
if err != nil {
return err
}
// Nested transaction (reuses the same transaction)
return transactor.WithinTransaction(outerTxCtx, func(innerTxCtx context.Context) error {
// Inner operations use the same transaction
return userRepo.Update(innerTxCtx, user)
})
})The library provides powerful query scopes for common operations:
db.Scopes(crud.Paginate(page, limit)).Find(&users)
// Example: Get page 2 with 10 items per page
db.Scopes(crud.Paginate(2, 10)).Find(&users)// Order by name ascending
db.Scopes(crud.OrderBy("name", true)).Find(&users)
// Order by created_at descending
db.Scopes(crud.OrderBy("created_at", false)).Find(&users)
// Default ordering (created_at DESC)
db.Scopes(crud.DefaultOrder()).Find(&users)// Filter by specification
spec := User{Age: 25, Name: "Alice"}
db.Scopes(crud.WhereBySpec(spec)).Find(&users)
// Time range filtering
start := time.Now().Add(-24 * time.Hour)
end := time.Now()
db.Scopes(crud.BetweenTime("created_at", start, end)).Find(&users)relations := []string{"Profile", "Posts", "Comments"}
db.Scopes(crud.PreloadRelations(relations)).Find(&users)// Add FOR UPDATE clause
db.Scopes(crud.ForUpdate(true)).First(&user, id)// Complex query with multiple scopes
db.Scopes(
crud.WhereBySpec(User{Age: 25}),
crud.OrderBy("name", true),
crud.Paginate(1, 10),
crud.PreloadRelations([]string{"Profile"}),
).Find(&users)The library includes robust field name validation to prevent SQL injection:
// Safe field names (allowed)
"name" β
"created_at" β
"users.name" β
"table.column" β
// Dangerous field names (rejected)
"name'; DROP TABLE users; --" β
"name' OR '1'='1" β
".invalid" β
"invalid." β
"table..column" β- Only ASCII letters, digits, underscores, and dots allowed
- No leading or trailing dots
- No consecutive dots
- No empty strings
- No special characters or SQL keywords
The library comes with comprehensive tests achieving 84.3% coverage:
# Run all tests
go test -v ./test/...
# Run tests with coverage
go test -v -coverprofile=coverage.out -coverpkg=./... ./test/...
# Generate coverage report
go tool cover -html=coverage.out -o coverage.html
# Use the test runner script
./test/run_tests.sh- Unit Tests: All public functions and methods
- Integration Tests: Database operations with SQLite
- Security Tests: SQL injection prevention
- Concurrency Tests: Thread safety validation
- Performance Tests: Benchmarks for critical functions
go-crud/
βββ gorm_crud_repository.go # Main CRUD repository implementation
βββ gorm_scopes.go # Query scopes and builders
βββ gorm_transactor.go # Transaction management
βββ internal/
β βββ gorm.go # Internal GORM utilities
β βββ transactor.go # Internal transaction logic
βββ lib/
β βββ constants.go # Library constants
βββ test/ # Comprehensive test suite
βββ crud_repository_test.go
βββ scopes_test.go
βββ transactor_test.go
βββ internal_test.go
βββ constants_test.go
βββ run_tests.sh
The library works with any GORM-supported database:
// PostgreSQL
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
// MySQL
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// SQLite
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
NamingStrategy: schema.NamingStrategy{
TablePrefix: "app_",
SingularTable: false,
},
})// Instead of multiple Insert calls
users := []User{{Name: "Alice"}, {Name: "Bob"}}
userRepo.BatchInsert(ctx, users)// Only preload what you need
spec := crud.Specification[User]{
PreloadRelations: []string{"Profile"}, // Not all relations
}// For large datasets
db.Scopes(crud.Paginate(page, 50)).Find(&users)// Group related operations
transactor.WithinTransaction(ctx, func(txCtx context.Context) error {
// Multiple related operations
return nil
})Contributions are welcome! Please ensure:
- Tests: Add tests for new features
- Coverage: Maintain or improve test coverage
- Documentation: Update documentation for new features
- Security: Follow security best practices
- Performance: Consider performance implications
# Clone the repository
git clone https://github.com/itsLeonB/go-crud.git
cd go-crud
# Install dependencies
go mod tidy
# Run tests
make test
# Check coverage
make test-coverage-htmlThis project is licensed under the MIT License - see the LICENSE file for details.
- GORM: The fantastic ORM library that powers this project
- Testify: For making tests more readable and maintainable
- Eris: For enhanced error handling and stack traces
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: This README and inline code documentation
Built with β€οΈ for the Go community