gormplus

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2025 License: MIT Imports: 5 Imported by: 0

README

GORM Plus

A type-safe generic base model pattern implementation for GORM, providing a clean and consistent interface for common database operations.

Features

  • Type-safe operations: Generic implementation ensures compile-time type safety
  • Transaction support: Built-in transaction management with automatic rollback
  • Flexible querying: Composable scope functions for building complex queries
  • Pagination: Built-in pagination support with configurable page sizes
  • Soft delete support: Full support for GORM's soft delete functionality
  • Row locking: SELECT FOR UPDATE support for concurrent access control
  • Batch operations: Efficient batch insert operations with configurable batch sizes

Installation

go get github.com/nullcache/gorm-plus

Quick Start

package main

import (
    "context"
    "log"

    "github.com/nullcache/gorm-plus"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

type User struct {
    ID    uint   `gorm:"primaryKey"`
    Name  string `gorm:"not null"`
    Email string `gorm:"unique;not null"`
    Age   int    `gorm:"default:0"`
}

func main() {
    // Initialize database
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    // Auto migrate
    db.AutoMigrate(&User{})

    // Create base model
    userBaseModel, err := gormplus.NewBaseModel[User](db)
    if err != nil {
        log.Fatal(err)
    }

    ctx := context.Background()

    // Create a user
    user := &User{Name: "John Doe", Email: "john@example.com", Age: 30}
    err = userBaseModel.Create(ctx, nil, user)
    if err != nil {
        log.Fatal(err)
    }

    // Find user by email
    foundUser, err := userBaseModel.First(ctx, gormplus.Where("email = ?", "john@example.com"))
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("Found user: %+v", foundUser)
}

Core Components

Base Model

The BaseModel[T] type is the main component that provides database operations for entities of type T.

// Create a new base model instance
userBaseModel, err := gormplus.NewBaseModel[User](db)
Scopes

Scopes are composable functions that modify GORM queries:

// Basic conditions
users, err := userBaseModel.List(ctx,
    gormplus.Where("age > ?", 25),
    gormplus.Order("name ASC"),
    gormplus.Limit(10),
)

// Map-based conditions
users, err := userBaseModel.List(ctx,
    gormplus.WhereEq(map[string]any{
        "active": true,
        "role":   "admin",
    }),
)
Available Scopes
  • Where(query, args...) - Add WHERE conditions
  • WhereEq(map[string]any) - Add equality conditions from map
  • Order(string) - Add ORDER BY clause
  • Select(columns...) - Select specific columns
  • Limit(int) - Limit number of results
  • Offset(int) - Skip number of results
  • WithDeleted() - Include soft-deleted records
  • OnlyDeleted() - Only soft-deleted records

Operations

CRUD Operations
// Create
user := &User{Name: "Jane Doe", Email: "jane@example.com"}
err := userBaseModel.Create(ctx, nil, user)

// Update
user.Age = 25
err = userBaseModel.Update(ctx, nil, user)

// Delete (soft delete if DeletedAt field exists)
err = userBaseModel.Delete(ctx, nil, gormplus.Where("id = ?", user.ID))
Query Operations
// Find first record
user, err := userBaseModel.First(ctx, gormplus.Where("email = ?", "john@example.com"))

// List records
users, err := userBaseModel.List(ctx,
    gormplus.Where("age > ?", 18),
    gormplus.Order("name ASC"),
)

// Count records
count, err := userBaseModel.Count(ctx, gormplus.Where("active = ?", true))

// Check existence
exists, err := userBaseModel.Exists(ctx, gormplus.Where("email = ?", "test@example.com"))
Pagination
// Get paginated results
result, err := userBaseModel.Page(ctx, 1, 20, // page 1, 20 items per page
    gormplus.Where("active = ?", true),
    gormplus.Order("created_at DESC"),
)

// Access pagination info
fmt.Printf("Page: %d/%d, Total: %d, HasNext: %v",
    result.Page,
    (result.Total + int64(result.PageSize) - 1) / int64(result.PageSize),
    result.Total,
    result.HasNext,
)
Batch Operations
users := []*User{
    {Name: "User 1", Email: "user1@example.com"},
    {Name: "User 2", Email: "user2@example.com"},
    {Name: "User 3", Email: "user3@example.com"},
}

// Batch insert with default batch size (1000)
err := userBaseModel.BatchInsert(ctx, nil, users)

// Batch insert with custom batch size
err = userBaseModel.BatchInsert(ctx, nil, users, 100)
Transactions
// Manual transaction management
err := userBaseModel.Transact(ctx, func(ctx context.Context, tx *gorm.DB) error {
    user1 := &User{Name: "User 1", Email: "user1@example.com"}
    if err := userBaseModel.Create(ctx, tx, user1); err != nil {
        return err
    }

    user2 := &User{Name: "User 2", Email: "user2@example.com"}
    if err := userBaseModel.Create(ctx, tx, user2); err != nil {
        return err
    }

    return nil // Commit transaction
})

// Row locking (requires transaction)
err = db.Transaction(func(tx *gorm.DB) error {
    user, err := userBaseModel.FirstForUpdate(ctx, tx, gormplus.Where("id = ?", 1))
    if err != nil {
        return err
    }

    // Modify user safely
    user.Balance += 100
    return userBaseModel.Update(ctx, tx, &user)
})

Error Handling

The library defines several standard errors:

// Check for specific errors
user, err := userBaseModel.First(ctx, gormplus.Where("id = ?", 999))
if errors.Is(err, gormplus.ErrNotFound) {
    // Handle not found case
}

// Available errors:
// - gormplus.ErrInvalidType: Invalid generic type
// - gormplus.ErrNotFound: Record not found
// - gormplus.ErrTxRequired: Transaction required for operation
// - gormplus.ErrDangerous: Dangerous operation (e.g., delete without conditions)

Best Practices

  1. Always use contexts: Pass context for cancellation and timeout support
  2. Handle transactions properly: Use the Transact method for complex operations
  3. Validate inputs: Check for required conditions before dangerous operations
  4. Use appropriate scopes: Combine scopes to build precise queries
  5. Handle errors: Always check for specific error types when needed

Requirements

  • Go 1.19 or higher
  • GORM v1.25.0 or higher

License

MIT License. See LICENSE file for details.

Contributing

Contributions are welcome. Please ensure all tests pass and follow the existing code style.

Documentation

Overview

Package gormplus provides a generic base model pattern implementation for GORM. It offers a type-safe wrapper around GORM operations with support for transactions, scoped queries, pagination, and common CRUD operations.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidType is returned when the generic type T is not a struct type.
	ErrInvalidType = errors.New("generic type must be a struct type")

	// ErrNotFound is returned when a requested record is not found.
	ErrNotFound = errors.New("not found")

	// ErrTxRequired is returned when a transaction is required but not provided.
	ErrTxRequired = errors.New("tx is required")

	// ErrDangerous is returned when attempting potentially dangerous operations
	// like deleting without conditions.
	ErrDangerous = errors.New("dangerous operation is prohibited")
)

Common errors returned by base model operations.

Functions

This section is empty.

Types

type BaseModel added in v0.2.0

type BaseModel[T any] struct {
	// contains filtered or unexported fields
}

BaseModel is a generic base model that provides common database operations for entities of type T. It wraps a GORM database instance and provides type-safe methods for CRUD operations, querying, and transaction handling.

func NewBaseModel added in v0.2.0

func NewBaseModel[T any](db *gorm.DB) (*BaseModel[T], error)

NewBaseModel creates a new generic base model instance for type T. It validates that T is a struct type. Returns an error if T is not a valid struct type.

func (*BaseModel[T]) BatchInsert added in v0.2.0

func (r *BaseModel[T]) BatchInsert(ctx context.Context, tx *gorm.DB, ents []*T, batchSize ...int) error

BatchInsert performs a batch insert operation for multiple entities. If tx is provided, the operation is performed within that transaction. The optional batchSize parameter controls how many records are inserted in each batch. If not specified or zero, defaults to 1000 records per batch.

func (*BaseModel[T]) Count added in v0.2.0

func (r *BaseModel[T]) Count(ctx context.Context, scopes ...Scope) (int64, error)

Count returns the number of records that match the provided scopes.

func (*BaseModel[T]) Create added in v0.2.0

func (r *BaseModel[T]) Create(ctx context.Context, tx *gorm.DB, ent *T) error

Create inserts a new entity into the database. If tx is provided, the operation is performed within that transaction. Otherwise, it uses the base model's default database connection.

func (*BaseModel[T]) Delete added in v0.2.0

func (r *BaseModel[T]) Delete(ctx context.Context, tx *gorm.DB, scopes ...Scope) error

Delete removes records from the database based on the provided conditions. At least one scope must be provided to prevent accidental deletion of all records. If tx is provided, the operation is performed within that transaction.

func (*BaseModel[T]) Exists added in v0.2.0

func (r *BaseModel[T]) Exists(ctx context.Context, scopes ...Scope) (bool, error)

Exists checks whether any record matching the provided scopes exists. Returns true if at least one record exists, false otherwise.

func (*BaseModel[T]) FindForUpdate added in v0.2.0

func (r *BaseModel[T]) FindForUpdate(ctx context.Context, tx *gorm.DB, scopes ...Scope) ([]T, error)

FindForUpdate retrieves all records that match the provided scopes with a SELECT FOR UPDATE lock. This method requires a transaction to be provided. Returns ErrTxRequired if no transaction is provided.

func (*BaseModel[T]) First added in v0.2.0

func (r *BaseModel[T]) First(ctx context.Context, scopes ...Scope) (T, error)

First retrieves the first record that matches the provided scopes. Returns ErrNotFound if no record is found.

func (*BaseModel[T]) FirstForUpdate added in v0.2.0

func (r *BaseModel[T]) FirstForUpdate(ctx context.Context, tx *gorm.DB, scopes ...Scope) (T, error)

FirstForUpdate retrieves the first record that matches the provided scopes with a SELECT FOR UPDATE lock. This method requires a transaction to be provided. Returns ErrNotFound if no record is found, ErrTxRequired if no transaction is provided.

func (*BaseModel[T]) List added in v0.2.0

func (r *BaseModel[T]) List(ctx context.Context, scopes ...Scope) ([]T, error)

List retrieves all records that match the provided scopes. Consider using Limit and Order scopes to control the result set size and ordering.

func (*BaseModel[T]) Page added in v0.2.0

func (r *BaseModel[T]) Page(ctx context.Context, page, pageSize int, scopes ...Scope) (PageResult[T], error)

Page retrieves a paginated result set based on the provided scopes. Page numbers are 1-based. If page <= 0, defaults to 1. If pageSize <= 0, defaults to 20. Maximum pageSize is capped at 1000.

func (*BaseModel[T]) Transact added in v0.2.0

func (r *BaseModel[T]) Transact(ctx context.Context, fn func(ctx context.Context, tx *gorm.DB) error) error

Transact executes the provided function within a database transaction. If the function returns an error, the transaction is rolled back. Otherwise, the transaction is committed.

func (*BaseModel[T]) Update added in v0.2.0

func (r *BaseModel[T]) Update(ctx context.Context, tx *gorm.DB, ent *T) error

Update saves the entity to the database, updating all fields. If tx is provided, the operation is performed within that transaction. Otherwise, it uses the base model's default database connection.

func (*BaseModel[T]) UpdateColumn added in v0.2.0

func (r *BaseModel[T]) UpdateColumn(ctx context.Context, tx *gorm.DB, column string, value any, scopes ...Scope) error

UpdateColumn updates a single column for records matching the provided scopes. At least one scope must be provided to prevent accidental update of all records. If tx is provided, the operation is performed within that transaction.

func (*BaseModel[T]) UpdateColumns added in v0.2.0

func (r *BaseModel[T]) UpdateColumns(ctx context.Context, tx *gorm.DB, updates any, scopes ...Scope) error

UpdateColumns updates multiple columns for records matching the provided scopes. At least one scope must be provided to prevent accidental update of all records. If tx is provided, the operation is performed within that transaction. The updates parameter can be a map[string]any or a struct.

type PageResult

type PageResult[T any] struct {
	Items    []T   `json:"items"`     // The items in the current page
	Total    int64 `json:"total"`     // Total number of items across all pages
	Page     int   `json:"page"`      // Current page number (1-based)
	PageSize int   `json:"page_size"` // Number of items per page
	HasNext  bool  `json:"has_next"`  // Whether there are more pages available
}

PageResult represents the result of a paginated query.

type Scope

type Scope func(*gorm.DB) *gorm.DB

Scope represents a function that can modify a GORM database query. Scopes can be chained together to build complex queries in a composable way.

func Limit

func Limit(n int) Scope

Limit creates a scope that limits the number of returned records.

func Offset

func Offset(n int) Scope

Offset creates a scope that skips the specified number of records.

func OnlyDeleted

func OnlyDeleted() Scope

OnlyDeleted creates a scope that returns only soft-deleted records.

func Order

func Order(order string) Scope

Order creates a scope that adds an ORDER BY clause to the query.

func Select

func Select(cols ...string) Scope

Select creates a scope that specifies which columns to select.

func Where

func Where(query any, args ...any) Scope

Where creates a scope that adds a WHERE clause to the query. It accepts the same parameters as GORM's Where method.

func WhereEq

func WhereEq(m map[string]any) Scope

WhereEq creates a scope that adds WHERE clauses for exact matches using a map of column names to values.

func WithDeleted

func WithDeleted() Scope

WithDeleted creates a scope that includes soft-deleted records in the query. This is equivalent to GORM's Unscoped method.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL