repository

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2025 License: MIT Imports: 2 Imported by: 1

README

Go Repository

A type-safe generic repository pattern implementation for Go, providing a clean and consistent interface for database operations. The library defines a generic repository interface and provides GORM-based implementation, with support for additional implementations.

Features

  • Type-safe operations: Generic implementation ensures compile-time type safety
  • Interface-based design: Clean separation between interface and implementation
  • 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
  • Comprehensive testing: Includes both interface contract tests and implementation-specific tests

Architecture

go-repo/
├── repository.go              # Generic repository interface
├── types.go                  # Common types (PageResult, etc.)
├── errors.go                 # Error definitions
└── gorm/                     # GORM implementation
    ├── repository.go         # GORM-specific implementation
    ├── repository_test.go    # GORM implementation tests
    └── testutils.go          # Test utilities and contract tests

Installation

go get github.com/nullcache/go-repo

Quick Start

package main

import (
    "context"
    "log"

    "github.com/nullcache/go-repo"
    gormrepo "github.com/nullcache/go-repo/gorm"
    "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 repository using the interface
    var userRepo repository.Repository[User]
    userRepo, err = gormrepo.NewRepository[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 = userRepo.Create(ctx, nil, user)
    if err != nil {
        log.Fatal(err)
    }

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

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

Core Components

Repository Interface

The Repository[T] interface defines the contract for all repository implementations:

type Repository[T any] interface {
    // CRUD Operations
    Create(ctx context.Context, tx Transaction, entity *T) error
    Update(ctx context.Context, tx Transaction, entity *T) error
    Delete(ctx context.Context, tx Transaction, scopes ...Scope) error

    // Query Operations
    First(ctx context.Context, scopes ...Scope) (T, error)
    List(ctx context.Context, scopes ...Scope) ([]T, error)
    Count(ctx context.Context, scopes ...Scope) (int64, error)
    Exists(ctx context.Context, scopes ...Scope) (bool, error)

    // Pagination
    Page(ctx context.Context, page, pageSize int, scopes ...Scope) (PageResult[T], error)

    // Batch Operations
    BatchInsert(ctx context.Context, tx Transaction, entities []*T, batchSize ...int) error

    // Transaction and Locking Operations
    Transact(ctx context.Context, fn func(ctx context.Context, tx Transaction) error) error
    FirstForUpdate(ctx context.Context, tx Transaction, scopes ...Scope) (T, error)
    FindForUpdate(ctx context.Context, tx Transaction, scopes ...Scope) ([]T, error)
}
GORM Implementation

The GORM implementation provides all the functionality with GORM-specific optimizations:

// Create a GORM repository
userRepo, err := gormrepo.NewRepository[User](db)
Scopes

Scopes are composable functions that modify queries. The GORM implementation provides:

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

// Map-based conditions
users, err := userRepo.List(ctx,
    gormrepo.WhereEq(map[string]any{
        "active": true,
        "role":   "admin",
    }),
)
Available GORM 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 := userRepo.Create(ctx, nil, user)

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

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

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

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

// Check existence
exists, err := userRepo.Exists(ctx, gormrepo.Where("email = ?", "test@example.com"))
Pagination
// Get paginated results
result, err := userRepo.Page(ctx, 1, 20, // page 1, 20 items per page
    gormrepo.Where("active = ?", true),
    gormrepo.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 := userRepo.BatchInsert(ctx, nil, users)

// Batch insert with custom batch size
err = userRepo.BatchInsert(ctx, nil, users, 100)
Transactions
// Transaction with automatic rollback on error
err := userRepo.Transact(ctx, func(ctx context.Context, tx repository.Transaction) error {
    user1 := &User{Name: "User 1", Email: "user1@example.com"}
    if err := userRepo.Create(ctx, tx, user1); err != nil {
        return err
    }

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

    return nil // Commit transaction
})

// Row locking (requires transaction)
err = userRepo.Transact(ctx, func(ctx context.Context, tx repository.Transaction) error {
    user, err := userRepo.FirstForUpdate(ctx, tx, gormrepo.Where("id = ?", 1))
    if err != nil {
        return err
    }

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

Error Handling

The library defines several standard errors:

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

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

Testing

The library provides comprehensive testing utilities:

Using the Generic Test Suite

The test utilities are provided in the gorm package. You can use them to test your repository implementations:

func TestMyRepository(t *testing.T) {
    // Test utilities are available in the gorm package
    // See gorm/testutils.go for available test helpers and models
}
Test Structure
  • Interface tests: Verify interface compliance and basic contract
  • Implementation tests: Test specific implementation features
  • Integration tests: End-to-end testing scenarios

Extending with New Implementations

To add support for other ORMs or database libraries:

  1. Implement the Repository[T] interface
  2. Create implementation-specific scope functions
  3. Add tests using the generic test suite
  4. Provide implementation-specific scopes and utilities

Example structure for a new implementation:

sqlx/
├── repository.go      # SQLx implementation
├── repository_test.go # SQLx-specific tests
├── testutils.go      # SQLx-specific test utilities
└── scopes.go         # SQLx-specific scopes

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
  6. Test thoroughly: Use the generic test suite for new implementations

Requirements

  • Go 1.19 or higher
  • GORM v1.25.0 or higher (for GORM implementation)

License

MIT License. See LICENSE file for details.

Contributing

Contributions are welcome! Please:

  1. Follow the existing code style
  2. Add tests for new features
  3. Update documentation as needed
  4. Ensure all tests pass

For new implementations of the Repository interface, please include:

  • Full implementation of the interface
  • Comprehensive tests using the generic test suite
  • Documentation with usage examples
  • Implementation-specific scopes and utilities

Documentation

Overview

Package repository provides a generic repository pattern interface for database operations. It offers a type-safe wrapper around database 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")

	// ErrNilSchema is returned when database schema parsing results in a nil schema.
	ErrNilSchema = errors.New("nil schema")

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

Common errors returned by repository operations.

Functions

This section is empty.

Types

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 Repository

type Repository[T any] interface {

	// Create inserts a new entity into the database.
	// If tx is provided, the operation is performed within that transaction.
	Create(ctx context.Context, tx Transaction, entity *T) error

	// Update saves the entity to the database, updating all fields.
	// If tx is provided, the operation is performed within that transaction.
	Update(ctx context.Context, tx Transaction, entity *T) 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.
	Delete(ctx context.Context, tx Transaction, scopes ...Scope) error

	// First retrieves the first record that matches the provided scopes.
	// Returns ErrNotFound if no record is found.
	First(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.
	List(ctx context.Context, scopes ...Scope) ([]T, error)

	// Count returns the number of records that match the provided scopes.
	Count(ctx context.Context, scopes ...Scope) (int64, error)

	// Exists checks whether any record matching the provided scopes exists.
	// Returns true if at least one record exists, false otherwise.
	Exists(ctx context.Context, scopes ...Scope) (bool, 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.
	Page(ctx context.Context, page, pageSize int, scopes ...Scope) (PageResult[T], 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.
	BatchInsert(ctx context.Context, tx Transaction, entities []*T, batchSize ...int) 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.
	Transact(ctx context.Context, fn func(ctx context.Context, tx Transaction) error) 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.
	FirstForUpdate(ctx context.Context, tx Transaction, 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.
	FindForUpdate(ctx context.Context, tx Transaction, scopes ...Scope) ([]T, error)
}

Repository defines the generic repository interface for database operations on entities of type T. It provides type-safe methods for CRUD operations, querying, and transaction handling.

type Scope

type Scope func(query any) any

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

type Transaction

type Transaction interface{}

Transaction represents a database transaction. The specific implementation depends on the underlying database driver.

Directories

Path Synopsis
gorm module

Jump to

Keyboard shortcuts

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