thing

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: May 4, 2025 License: MIT Imports: 20 Imported by: 0

README

Thing ORM: High-Performance Go ORM with Built-in Caching

Go Report Card Build Status Go Reference MIT License Go Version

Thing ORM is a high-performance, open-source Object-Relational Mapper for Go, designed for modern application needs:

  • Unique Integrated Caching: Thing ORM provides built-in support for either Redis or in-memory caching, making cache integration seamless and efficient. It automatically caches single-entity and list queries, manages cache invalidation, and provides cache hit/miss monitoring—out of the box. No third-party plugins or manual cache wiring required.
  • Type-Safe Generics-Based API: Built on Go generics, Thing ORM provides compile-time type safety, better performance, and a more intuitive, IDE-friendly API—unlike most Go ORMs that rely on reflection and runtime type checks.
  • Multi-Database Support: Effortlessly switch between MySQL, PostgreSQL, and SQLite with a unified API and automatic SQL dialect adaptation.
  • Simple, Efficient CRUD and List Queries: Focused on the most common application patterns—thread-safe Create, Read, Update, Delete, and efficient list retrieval with filtering, ordering, and pagination.
  • Focused API: Designed for fast CRUD and list operations. Complex SQL features like JOINs are out of the ORM's direct scope, but raw SQL execution is supported.
  • Elegant, Developer-Friendly API: Clean, extensible, and idiomatic Go API, with flexible JSON serialization, relationship management, and hooks/events system.
  • Open Source and Community-Ready: Well-documented, thoroughly tested, and designed for easy adoption and contribution by the Go community.

Table of Contents

Installation

go get github.com/burugo/thing

Configuration

Thing ORM is configured by providing a database adapter (DBAdapter) and an optional cache client (CacheClient) when creating a Thing instance using thing.New. This allows for flexible setup tailored to your application's needs.

1. Choose a Database Adapter

First, create an instance of a database adapter for your chosen database (MySQL, PostgreSQL, or SQLite).

import (
	// "github.com/burugo/thing/drivers/db/mysql"
	// "github.com/burugo/thing/drivers/db/postgres"
	"github.com/burugo/thing/drivers/db/sqlite"
)

// Example: SQLite (replace ":memory:" with your file path)
dbAdapter, err := sqlite.NewSQLiteAdapter(":memory:")
if err != nil {
	log.Fatal("Failed to create SQLite adapter:", err)
}

// Example: MySQL (replace with your actual DSN)
// mysqlDSN := "user:password@tcp(127.0.0.1:3306)/database?parseTime=true"
// dbAdapter, err := mysql.NewMySQLAdapter(mysqlDSN)
// if err != nil {
// 	log.Fatal("Failed to create MySQL adapter:", err)
// }

// Example: PostgreSQL (replace with your actual DSN)
// pgDSN := "host=localhost user=user password=password dbname=database port=5432 sslmode=disable TimeZone=Asia/Shanghai"
// dbAdapter, err := postgres.NewPostgreSQLAdapter(pgDSN)
// if err != nil {
// 	log.Fatal("Failed to create PostgreSQL adapter:", err)
// }
2. Choose a Cache Client (Optional)

Thing ORM includes a built-in in-memory cache, which is used by default if no cache client is provided. For production or distributed systems, using Redis is recommended.

import (
	"github.com/redis/go-redis/v9"
	redisCache "github.com/burugo/thing/drivers/cache/redis"
	"github.com/burugo/thing"
)

// Option A: Use Default In-Memory Cache
// Simply pass nil as the cache client when calling thing.New
var cacheClient thing.CacheClient = nil

// Option B: Use Redis
// redisAddr := "localhost:6379"
// redisPassword := ""
// redisDB := 0
// rdb := redis.NewClient(&redis.Options{
// 	Addr:     redisAddr,
// 	Password: redisPassword,
// 	DB:       redisDB,
// })
// cacheClient = redisCache.NewClient(rdb) // Create Thing's Redis client wrapper

3. Create Thing Instance

Use thing.New to create an ORM instance for your specific model type, passing the chosen database adapter and cache client.

import (
	"github.com/burugo/thing"
	// import your models package
)

// Create a Thing instance for the User model
userThing, err := thing.New[*models.User](dbAdapter, cacheClient)
if err != nil {
	log.Fatal("Failed to create Thing instance for User:", err)
}

// Now you can use userThing for CRUD, queries, etc.
// userThing.Save(...)
// userThing.ByID(...)
// userThing.Query(...)
Global Configuration (Alternative)

For simpler applications or global setup, you can use thing.Configure once at startup and then thing.Use to get model-specific instances. Note: This uses global variables and is less flexible for managing multiple database/cache connections.

// At application startup:
// err := thing.Configure(dbAdapter, cacheClient)
// if err != nil { ... }

// Later, in your code:
// userThing, err := thing.Use[*models.User]()
// if err != nil { ... }

API Documentation

Full API documentation is available on pkg.go.dev.

(Note: The documentation link will become active after the first official release/tag of the package.)

AI Assistant Integration (e.g., Cursor)

For developers using AI coding assistants like Cursor, a dedicated project rule file is available to help the AI understand and correctly utilize the Thing ORM within this project.

You can find the rule file here: Thing ORM Cursor Rules

Referencing this rule (@thing) in your prompts can improve the AI's accuracy when generating or modifying code related to Thing ORM.

Basic CRUD Example

Here is a minimal example demonstrating how to use Thing ORM for basic CRUD operations:

package main

import (
	"fmt"
	"log"

	"github.com/burugo/thing"
)

// User model definition
// Only basic fields for demonstration
// No relationships

type User struct {
	thing.BaseModel
	Name string `db:"name"`
	Age  int    `db:"age"`
}

func main() {

	// Configure Thing ORM (in-memory DB and cache for demo)
	thing.Configure()

	// Auto-migrate User table
	if err := thing.AutoMigrate(&User{}); err != nil {
		log.Fatal(err)
	}

	// Get the User ORM object
	users, err := thing.Use[*User]()

	// Create
	u := &User{Name: "Alice", Age: 30}
	if err := users.Save(u); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Created:", u)

	// ByID
	found, err := users.ByID(u.ID)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("ByID:", found)

	// Update
	found.Age = 31
	if err := users.Save(found); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Updated:", found)

	// Query all
	result, err := users.Query(thing.QueryParams{})
	if err != nil {
		log.Fatal(err)
	}
	all, err := result.All()
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("All users:", all)

	// Delete
	if err := users.Delete(u); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Deleted user.")
}

Flexible JSON Serialization

Thing ORM provides multiple ways to control JSON output fields, order, and nesting:

  • Include(fields ...string): Specify exactly which top-level fields to output, in order. Best for simple, flat cases.
  • Exclude(fields ...string): Specify top-level fields to exclude from output. Can be combined with Include or used alone.
  • WithFields(dsl string): Use a powerful DSL string to control inclusion, exclusion, order, and nested fields (e.g. "name,profile{avatar},-id").

Note:

  • Include and Exclude only support flat (top-level) fields.
  • For nested field control, use WithFields DSL.
  • You can combine Include, Exclude, and WithFields, but only WithFields supports nested and ordered field selection.
Examples
// Only include id, name, and full_name (method-based virtual)
thing.ToJSON(user, thing.Include("id", "name", "full_name"))

// Exclude sensitive fields
thing.ToJSON(user, thing.Exclude("password", "email"))

// Combine Include and Exclude (still only affects top-level fields)
thing.ToJSON(user, thing.Include("id", "name", "email"), thing.Exclude("email"))

// Use WithFields DSL for advanced/nested control
thing.ToJSON(user, thing.WithFields("name,profile{avatar},-id"))
  • WithFields supports nested fields, exclusion (with -field), and output order.
  • Include/Exclude are Go-idiomatic and best for simple, flat cases.
  • Struct tags (e.g. json:"-") always take precedence.
Method-based Virtual Properties

You can define computed (virtual) fields on your model by adding exported, zero-argument, single-return-value methods. These methods will only be included in the JSON output if you explicitly reference their corresponding field name in the DSL string or Include option.

  • Method Naming: Use Go's exported method naming (e.g., FullName). The field name in the DSL should be the snake_case version (e.g., full_name).
  • How it works:
    • If the DSL or Include includes a field name that matches a method (converted to snake_case), the method will be called and its return value included in the output.
    • If the DSL/Include does not mention the virtual field, it will not be output.

Example:

type User struct {
    FirstName string
    LastName  string
}

// Virtual property method
func (u *User) FullName() string {
    return u.FirstName + " " + u.LastName
}

user := &User{FirstName: "Alice", LastName: "Smith"}
jsonBytes, _ := thing.ToJSON(user, thing.WithFields("first_name,full_name"))
fmt.Println(string(jsonBytes))
// Output: {"first_name":"Alice","full_name":"Alice Smith"}
  • If you omit full_name from the DSL or Include, the FullName() method will not be called or included in the output.

This approach gives you full control over which computed fields are exposed, and ensures only explicitly requested virtuals are included in the JSON output.

Relationship Management

Defining Relationships

Thing ORM supports basic relationship management, including preloading related models.

Use thing struct tags to define relationships. The db:"-" tag prevents the ORM from treating the relationship field as a database column.

package models // assuming models are in a separate package

import "github.com/burugo/thing"

// User has many Books
type User struct {
	thing.BaseModel
	Name  string `db:"name"`
	Email string `db:"email"`
	// Define HasMany relationship:
	// - fk: Foreign key in the 'Book' table (user_id)
	// - model: Name of the related model struct (Book)
	Books []*Book `thing:"hasMany;fk:user_id;model:Book" db:"-"`
}

func (u *User) TableName() string { return "users" }

// Book belongs to a User
type Book struct {
	thing.BaseModel
	Title  string `db:"title"`
	UserID int64  `db:"user_id"` // Foreign key column
	// Define BelongsTo relationship:
	// - fk: Foreign key in the 'Book' table itself (user_id)
	User *User `thing:"belongsTo;fk:user_id" db:"-"`
}

func (b *Book) TableName() string { return "books" }

Use the Preloads field in QueryParams to specify relationships to eager-load.

package main

import (
	"fmt"
	"log"

	"github.com/burugo/thing"
	// import your models package e.g., "yourproject/models"
)

func main() {
	// Assume thing.Configure() and AutoMigrate(&models.User{}, &models.Book{}) are done
	// Assume user and books are created...

	userThing, _ := thing.Use[*models.User]()
	bookThing, _ := thing.Use[*models.Book]()

	// Example 1: Find a user and preload their books (HasMany)
	userParams := thing.QueryParams{
		Where:    "id = ?",
		Args:     []interface{}{1}, // Assuming user with ID 1 exists
		Preloads: []string{"Books"}, // Specify the relationship field name
	}
	userResult, _ := userThing.Query(userParams)
	fetchedUsers, _ := userResult.Fetch(0, 1)
	if len(fetchedUsers) > 0 {
		fmt.Printf("User: %s, Number of Books: %d\n", fetchedUsers[0].Name, len(fetchedUsers[0].Books))
		// fetchedUsers[0].Books is now populated
	}

	// Example 2: Find a book and preload its user (BelongsTo)
	bookParams := thing.QueryParams{
		Where:    "id = ?",
		Args:     []interface{}{5}, // Assuming book with ID 5 exists
		Preloads: []string{"User"}, // Specify the relationship field name
	}
	bookResult, _ := bookThing.Query(bookParams)
	fetchedBooks, _ := bookResult.Fetch(0, 1)
	if len(fetchedBooks) > 0 && fetchedBooks[0].User != nil {
		fmt.Printf("Book: %s, Owner: %s\n", fetchedBooks[0].Title, fetchedBooks[0].User.Name)
		// fetchedBooks[0].User is now populated
	}
}

Thing ORM automatically fetches the related models in an optimized way, utilizing the cache where possible.

Hooks & Events

Thing ORM provides a hook system that allows you to register functions (listeners) to be executed before or after specific database operations. This is useful for tasks like validation, logging, data modification, or triggering side effects.

Available Events
  • EventTypeBeforeSave: Before creating or updating a record.
  • EventTypeAfterSave: After successfully creating or updating a record.
  • EventTypeBeforeCreate: Before creating a new record (subset of BeforeSave).
  • EventTypeAfterCreate: After successfully creating a new record.
  • EventTypeBeforeDelete: Before hard deleting a record.
  • EventTypeAfterDelete: After successfully hard deleting a record.
  • EventTypeBeforeSoftDelete: Before soft deleting a record.
  • EventTypeAfterSoftDelete: After successfully soft deleting a record.
Registering Listeners

Use thing.RegisterListener to attach your hook function to an event type. The listener function receives the context, event type, the model instance, and optional event-specific data.

Listener Signature:

func(ctx context.Context, eventType thing.EventType, model interface{}, eventData interface{}) error
  • Returning an error from a Before* hook will abort the database operation.
  • eventData for EventTypeAfterSave contains a map[string]interface{} of changed fields.
Example
package main

import (
	"context"
	"errors"
	"fmt"
	"log"

	"github.com/burugo/thing"
	// Assume User model is defined
)

// Example Hook: Validate email before saving
func validateEmailHook(ctx context.Context, eventType thing.EventType, model interface{}, eventData interface{}) error {
	if user, ok := model.(*User); ok { // Type assert to your model
		log.Printf("[HOOK %s] Checking user: %s, Email: %s", eventType, user.Name, user.Email)
		if user.Email == "invalid@example.com" {
			return errors.New("invalid email provided")
		}
	}
	return nil
}

// Example Hook: Log after creation
func logAfterCreateHook(ctx context.Context, eventType thing.EventType, model interface{}, eventData interface{}) error {
	if user, ok := model.(*User); ok {
		log.Printf("[HOOK %s] User created! ID: %d, Name: %s", eventType, user.ID, user.Name)
	}
	return nil
}

func main() {
	// Assume thing.Configure() and thing.AutoMigrate(&User{}) are done

	// Register hooks
	thing.RegisterListener(thing.EventTypeBeforeSave, validateEmailHook)
	thing.RegisterListener(thing.EventTypeAfterCreate, logAfterCreateHook)

	// Get ORM instance
	users, _ := thing.Use[*User]()

	// 1. Attempt to save user with invalid email (will be aborted by hook)
	invalidUser := &User{Name: "Invalid", Email: "invalid@example.com"}
	err := users.Save(invalidUser)
	if err != nil {
		fmt.Printf("Failed to save invalid user (as expected): %v\n", err)
	}

	// 2. Save a valid user (triggers BeforeSave and AfterCreate hooks)
	validUser := &User{Name: "Valid Hook User", Email: "valid@example.com"}
	err = users.Save(validUser)
	if err != nil {
		log.Fatalf("Failed to save valid user: %v", err)
	} else {
		fmt.Printf("Successfully saved valid user ID: %d\n", validUser.ID)
	}

	// Unregistering listeners is also possible with thing.UnregisterListener
}

Caching & Monitoring

Cache Monitoring & Hit/Miss Statistics

Thing ORM provides built-in cache operation monitoring for all cache clients (including Redis and the mock client used in tests). This monitoring capability is a core, integrated feature, not an add-on.

You can call GetCacheStats(ctx) on any CacheClient instance to retrieve a snapshot of cache operation counters. The returned CacheStats.Counters map includes keys such as:

  • Get: total number of Get calls
  • GetMiss: number of Get calls that missed (not found)
  • GetModel, GetModelMiss: same for model cache
  • GetQueryIDs, GetQueryIDsMiss: same for query ID list cache

To compute hit count and hit rate:

  • hit = total - miss
  • hit rate = hit / total

Example:

stats := cacheClient.GetCacheStats(ctx)
getTotal := stats.Counters["Get"]
getMiss := stats.Counters["GetMiss"]
getHit := getTotal - getMiss
hitRate := float64(getHit) / float64(getTotal)
fmt.Printf("Get hit rate: %.2f%%\n", hitRate*100)

This mechanism allows you to monitor cache effectiveness, debug performance issues, and tune your caching strategy.

Note: All counters are thread-safe and represent the state since the cache client was created or last reset. This built-in monitoring applies automatically to any cache backend you use (Redis, in-memory, etc.).

Advanced Usage

Raw SQL Execution

While Thing ORM focuses on common CRUD and list patterns, you can always drop down to raw SQL for complex queries or operations not directly supported by the ORM API. You can access the underlying DBAdapter or even the standard *sql.DB connection.

package main

import (
	"context"
	"database/sql"
	"fmt"
	"log"

	"github.com/burugo/thing"
	// import your models package
)

func rawSQLExample() {
	// Assume thing.Configure() is done and you have a User model
	userThing, _ := thing.Use[*models.User]()

	// Get the underlying DBAdapter
	dbAdapter := userThing.DBAdapter()
	ctx := context.Background() // Or your request context

	// 1. Execute a raw SQL command (e.g., UPDATE)
	updateQuery := "UPDATE users SET email = ? WHERE id = ?"
	result, err := dbAdapter.Exec(ctx, updateQuery, "new.raw.email@example.com", 1)
	if err != nil {
		log.Fatalf("Raw Exec failed: %v", err)
	}
	rowsAffected, _ := result.RowsAffected()
	fmt.Printf("Raw Exec: Rows affected: %d\n", rowsAffected)

	// 2. Query a single row and scan into a struct (using Adapter.Get)
	var user models.User
	selectQuery := "SELECT id, name, email FROM users WHERE id = ?"
	err = dbAdapter.Get(ctx, &user, selectQuery, 1)
	if err != nil {
		if err == sql.ErrNoRows {
			fmt.Println("Raw Get: User not found")
		} else {
			log.Fatalf("Raw Get failed: %v", err)
		}
	} else {
		fmt.Printf("Raw Get: User found: ID=%d, Name=%s\n", user.ID, user.Name)
	}

	// 3. Query multiple rows (using Adapter.Select)
	var names []string
	selectMultiple := "SELECT name FROM users WHERE email LIKE ? ORDER BY name"
	err = dbAdapter.Select(ctx, &names, selectMultiple, "%@example.com")
	if err != nil {
		log.Fatalf("Raw Select failed: %v", err)
	}
	fmt.Printf("Raw Select: Found %d names ending in @example.com: %v\n", len(names), names)

	// 4. Access the standard *sql.DB (for things not covered by adapter)
	// sqlDB := dbAdapter.DB()
	// // Use sqlDB for standard database/sql operations if needed
}

// In main(), call rawSQLExample()

## Schema/Migration Tools

### Usage Overview

- Use `thing.AutoMigrate` to automatically generate and execute CREATE TABLE SQL, adapting to the current database dialect (MySQL/PostgreSQL/SQLite).
- Supports declaring normal and unique indexes via struct tags.

### Index Declaration

- Normal index: add tag `thing:"index"` to the struct field
- Unique index: add tag `thing:"unique"` to the struct field

```go
// Example
 type User struct {
     ID    int64  `db:"id,pk"`
     Name  string `db:"name" thing:"index"`
     Email string `db:"email" thing:"unique"`
 }
Auto Migration Example
import "github.com/burugo/thing"

// Configure the database adapter
thing.Configure(dbAdapter, cacheClient)

// Auto create tables (including indexes)
err := thing.AutoMigrate(&User{})
if err != nil {
    panic(err)
}
  • During migration, CREATE TABLE and CREATE INDEX/UNIQUE INDEX statements are automatically generated.
  • Supports batch migration of multiple models: thing.AutoMigrate(&User{}, &Book{})

Multi-Database Testing

Thing ORM supports multiple database systems, including MySQL, PostgreSQL, and SQLite. This section provides guidelines and considerations for testing across different environments.

Considerations
  • Database Compatibility: Thing ORM is designed to work with the SQL dialects of MySQL, PostgreSQL, and SQLite. However, there might be differences in SQL syntax or features between these databases.
  • Migration Tools: Thing ORM's migration tools are database-agnostic and should work across all supported databases. However, you might need to adjust SQL statements or query results based on the specific database you're using.
  • Testing Strategy:
    • Unit Tests: Ensure that your tests cover different database scenarios.
    • Integration Tests: Test the ORM's functionality with a variety of databases.
    • Manual Testing: Test the ORM in different environments to ensure compatibility.
FAQ
  • Driver Customization: If you need to customize the behavior of a database driver, you can do so by implementing the DBAdapter interface.
  • Test Environment Configuration: Ensure that your test environment is set up correctly for database testing.
  • Interface Location: All database-related interfaces are located in the drivers/db package.

Contributing

We welcome contributions from the community! If you're interested in contributing to Thing ORM, please follow these steps:

  1. Fork the Repository:

  2. Clone the Repository:

    • Clone your forked repository to your local machine.
    git clone https://github.com/your-username/thing.git
    
  3. Create a New Branch:

    • Create a new branch for your changes.
    git checkout -b feature-new-feature
    
  4. Make Your Changes:

    • Implement your changes in the code.
  5. Commit Your Changes:

    • Commit your changes with a meaningful commit message.
    git commit -m "Added new feature"
    
  6. Push Your Changes:

    • Push your changes to your forked repository.
    git push origin feature-new-feature
    
  7. Create a Pull Request:

    • Go to your forked repository on GitHub.
    • Click the "Pull Request" button to create a pull request.

We'll review your pull request and merge it if it meets our standards.

Performance

Thing ORM is designed to be performant and efficient. The following sections provide information about its performance characteristics and how to optimize its usage.

Performance Considerations
  • Caching & Resource Usage: Thing ORM reduces database load by caching queries and entities. In development, in-memory cache is convenient and memory is rarely a concern. In production, however, excessive or unbounded in-memory caching can cause memory pressure or leaks. For production deployments, Redis is strongly recommended for reliability and scalability.
  • Query Execution: Thing ORM provides efficient query execution. Complex queries might still require manual optimization.
Optimizing Thing ORM
  • Caching Strategy:
    • Cache Invalidation: Thing ORM automatically invalidates cache entries when data changes.
    • Cache Size: Thing ORM uses in-memory caching. Monitor memory usage and adjust cache size as needed.
  • Query Execution:
    • Preloading: Thing ORM provides efficient query execution with preloading.
    • Raw SQL: Thing ORM supports raw SQL execution. Use it for complex queries or when Thing ORM's API doesn't meet your needs.

License

Thing ORM is released under the MIT License.

Documentation

Index

Constants

View Source
const (
	LockDuration   = 5 * time.Second
	LockRetryDelay = 50 * time.Millisecond
	LockMaxRetries = 5
)

Lock duration constants for cache locking

View Source
const (
	ByIDBatchSize = 100 // Size of batches for fetching by ID from DB
)

--- Constants used internally ---

Variables

This section is empty.

Functions

func AutoMigrate

func AutoMigrate(models ...interface{}) error

AutoMigrate 生成并执行建表 SQL,支持批量建表和 schema diff

func Configure

func Configure(args ...interface{}) error

Configure sets up the package-level database and cache clients, and the global cache TTL. Usage:

Configure(db) // uses provided DB, default local cache
Configure(db, cache) // uses provided DB and cache
Configure(db, cache, ttl) // uses all provided

This MUST be called once during application initialization before using Use[T].

func ConfigureWithConfig

func ConfigureWithConfig(cfg Config) error

ConfigureWithConfig sets up the package-level database and cache clients using a Config struct.

func GenerateCacheKey

func GenerateCacheKey(prefix, tableName string, params QueryParams) string

GenerateCacheKey generates a cache key for list or count queries with normalized arguments.

func GenerateMigrationSQL

func GenerateMigrationSQL(models ...interface{}) ([]string, error)

GenerateMigrationSQL 生成建表 SQL,但不执行,支持批量模型

func GenerateQueryHash

func GenerateQueryHash(params QueryParams) string

GenerateQueryHash generates a unique hash for a given query.

func NewThingByType

func NewThingByType(modelType reflect.Type, db DBAdapter, cache CacheClient) (interface{}, error)

NewThingByType creates a *Thing for a given model type (reflect.Type)

func RegisterIntrospectorFactory

func RegisterIntrospectorFactory(dialect string, factory IntrospectorFactory)

RegisterIntrospectorFactory registers a factory for a given dialect (e.g. "sqlite", "mysql", "postgres").

func RegisterListener

func RegisterListener(eventType EventType, listener EventListener)

RegisterListener adds a listener function for a specific event type.

func ResetListeners

func ResetListeners()

ResetListeners clears all registered event listeners. Primarily intended for use in tests.

func UnregisterListener

func UnregisterListener(eventType EventType, listenerToRemove EventListener)

UnregisterListener removes a specific listener function for a specific event type. It compares function pointers to find the listener to remove.

func WithLock

func WithLock(ctx context.Context, cache CacheClient, lockKey string, action func(ctx context.Context) error) error

WithLock acquires a lock, executes the action, and releases the lock.

Types

type BaseModel

type BaseModel struct {
	ID        int64     `json:"id" db:"id,pk"`              // Primary key (Added pk tag)
	CreatedAt time.Time `json:"created_at" db:"created_at"` // Timestamp for creation
	UpdatedAt time.Time `json:"updated_at" db:"updated_at"` // Timestamp for last update
	Deleted   bool      `json:"deleted" db:"deleted"`       // Soft delete flag
	// contains filtered or unexported fields
}

BaseModel provides common fields and functionality for database models. It should be embedded into specific model structs.

func (BaseModel) GetID

func (b BaseModel) GetID() int64

GetID returns the primary key value.

func (*BaseModel) IsNewRecord

func (b *BaseModel) IsNewRecord() bool

IsNewRecord returns whether this is a new record.

func (BaseModel) KeepItem

func (b BaseModel) KeepItem() bool

KeepItem checks if the record is considered active (not soft-deleted).

func (*BaseModel) SetID

func (b *BaseModel) SetID(id int64)

SetID sets the primary key value.

func (*BaseModel) SetNewRecordFlag

func (b *BaseModel) SetNewRecordFlag(isNew bool)

SetNewRecordFlag sets the internal isNewRecord flag.

func (BaseModel) TableName

func (b BaseModel) TableName() string

TableName returns the database table name for the model. Default implementation returns empty string, relying on getTableNameFromType. Override this method in your specific model struct for custom table names.

type CacheClient

type CacheClient interface {
	Get(ctx context.Context, key string) (string, error)
	Set(ctx context.Context, key string, value string, expiration time.Duration) error
	Delete(ctx context.Context, key string) error

	GetModel(ctx context.Context, key string, dest interface{}) error
	SetModel(ctx context.Context, key string, model interface{}, expiration time.Duration) error
	DeleteModel(ctx context.Context, key string) error

	GetQueryIDs(ctx context.Context, queryKey string) ([]int64, error)
	SetQueryIDs(ctx context.Context, queryKey string, ids []int64, expiration time.Duration) error
	DeleteQueryIDs(ctx context.Context, queryKey string) error

	AcquireLock(ctx context.Context, lockKey string, expiration time.Duration) (bool, error)
	ReleaseLock(ctx context.Context, lockKey string) error

	GetCacheStats(ctx context.Context) CacheStats
}

CacheClient defines the interface for cache drivers.

var DefaultLocalCache CacheClient = &localCache{}

DefaultLocalCache is the default in-memory cache client for Thing ORM.

type CacheKeyLockManagerInternal

type CacheKeyLockManagerInternal struct {
	// contains filtered or unexported fields
}

CacheKeyLockManagerInternal manages a map of mutexes, one for each cache key. It uses sync.Map for efficient concurrent access.

func NewCacheKeyLockManagerInternal

func NewCacheKeyLockManagerInternal() *CacheKeyLockManagerInternal

NewCacheKeyLockManagerInternal creates a new lock manager.

func (*CacheKeyLockManagerInternal) Lock

func (m *CacheKeyLockManagerInternal) Lock(key string)

Lock acquires the mutex associated with the given cache key. If the mutex does not exist, it is created. This operation blocks until the lock is acquired.

func (*CacheKeyLockManagerInternal) Unlock

func (m *CacheKeyLockManagerInternal) Unlock(key string)

Unlock releases the mutex associated with the given cache key.

type CacheStats

type CacheStats struct {
	Counters map[string]int // Operation name to count
}

CacheStats holds cache operation counters for monitoring.

type CachedResult

type CachedResult[T Model] struct {
	// contains filtered or unexported fields
}

CachedResult represents a cached query result with lazy loading capabilities. It allows for efficient querying with pagination and caching.

func (*CachedResult[T]) All

func (cr *CachedResult[T]) All() ([]T, error)

All retrieves all records matching the query. It first gets the total count and then fetches all records using Fetch.

func (*CachedResult[T]) Count

func (cr *CachedResult[T]) Count() (int64, error)

Count returns the total number of records matching the query. It utilizes caching to avoid redundant database calls.

func (*CachedResult[T]) Fetch

func (cr *CachedResult[T]) Fetch(offset, limit int) ([]T, error)

Fetch returns a subset of records starting from the given offset with the specified limit. It filters out soft-deleted items and triggers cache updates if inconsistencies are found. This implementation closely follows the CachedResult.fetch() logic: - It iteratively fetches batches from cache or DB - It filters items using KeepItem() - It dynamically calculates how many more items to fetch based on filtering results

func (*CachedResult[T]) First

func (cr *CachedResult[T]) First() (T, error)

func (*CachedResult[T]) WithDeleted

func (cr *CachedResult[T]) WithDeleted() *CachedResult[T]

WithDeleted returns a new CachedResult instance that will include soft-deleted records in its results.

type Config

type Config struct {
	DB    DBAdapter   // User must initialize and provide
	Cache CacheClient // User must initialize and provide
	TTL   time.Duration
}

Config holds configuration for the Thing ORM.

type DBAdapter

type DBAdapter interface {
	Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error
	Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error
	Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
	GetCount(ctx context.Context, tableName string, where string, args []interface{}) (int64, error)
	BeginTx(ctx context.Context, opts *sql.TxOptions) (Tx, error)
	Close() error
	DB() *sql.DB
	Builder() SQLBuilder
	DialectName() string
}

DBAdapter defines the interface for database drivers.

func GlobalDB

func GlobalDB() DBAdapter

GlobalDB returns the global DBAdapter (for internal use, e.g., AutoMigrate)

type Dialector

type Dialector interface {
	Quote(identifier string) string // Quote a SQL identifier (table/column name)
	Placeholder(index int) string   // Bind variable placeholder (e.g. ?, $1)
}

Dialector defines how to quote identifiers and bind variables for a specific SQL dialect.

type EventListener

type EventListener func(ctx context.Context, eventType EventType, model interface{}, eventData interface{}) error

EventListener defines the signature for functions that can listen to events.

type EventType

type EventType string

EventType defines the type for lifecycle events.

const (
	EventTypeBeforeSave       EventType = "BeforeSave"
	EventTypeAfterSave        EventType = "AfterSave"
	EventTypeBeforeCreate     EventType = "BeforeCreate"
	EventTypeAfterCreate      EventType = "AfterCreate"
	EventTypeBeforeDelete     EventType = "BeforeDelete"
	EventTypeAfterDelete      EventType = "AfterDelete"
	EventTypeBeforeSoftDelete EventType = "BeforeSoftDelete"
	EventTypeAfterSoftDelete  EventType = "AfterSoftDelete"
)

Standard lifecycle event types

type FieldRule

type FieldRule struct {
	Name   string
	Nested *JSONOptions // Nested options for this specific field
}

FieldRule represents a single included field and its potential nested options.

type IntrospectorFactory

type IntrospectorFactory func(DBAdapter) schema.Introspector

IntrospectorFactory is a function that returns a schema.Introspector for a given DBAdapter.

type JSONOption

type JSONOption func(*JSONOptions)

JSONOption defines the function signature for JSON serialization options.

func Exclude

func Exclude(fields ...string) JSONOption

Exclude specifies fields to exclude from the JSON output.

func Include

func Include(fields ...string) JSONOption

Include specifies fields to include in the JSON output (simple, flat field list; no DSL features).

func WithFields

func WithFields(dsl string) JSONOption

WithFields specifies fields to include/exclude using a DSL string (supports nested, exclude, etc.).

type JSONOptions

type JSONOptions struct {
	OrderedInclude []*FieldRule // Ordered list of fields to include (with possible nested rules)
	OrderedExclude []string     // Ordered list of fields to exclude
}

JSONOptions holds the options for JSON serialization.

func ParseFieldsDSL

func ParseFieldsDSL(dsl string) (*JSONOptions, error)

ParseFieldsDSL parses a DSL string and returns populated jsonOptions representing the rules.

type Model

type Model interface {
	KeepItem() bool
	GetID() int64
}

Model is the base interface for all ORM models.

type QueryParams

type QueryParams struct {
	Where          string
	Args           []interface{}
	Order          string
	Preloads       []string
	IncludeDeleted bool
}

QueryParams is the public query parameter type for Thing ORM queries.

type RelationshipOpts

type RelationshipOpts struct {
	RelationType   string // "belongsTo", "hasMany", "manyToMany"
	ForeignKey     string // FK field name in the *owning* struct (for belongsTo) or *related* struct (for hasMany)
	LocalKey       string // PK field name in the *owning* struct (defaults to info.pkName)
	RelatedModel   string // Optional: Specify related model name if different from field type
	JoinTable      string // For manyToMany: join table name
	JoinLocalKey   string // For manyToMany: join table column for local model
	JoinRelatedKey string // For manyToMany: join table column for related model
}

RelationshipOpts defines the configuration for a relationship based on struct tags.

type SQLBuilder

type SQLBuilder interface {
	BuildSelectSQL(tableName string, columns []string) string
	BuildSelectIDsSQL(tableName string, pkName string, where string, args []interface{}, order string) (string, []interface{})
	BuildInsertSQL(tableName string, columns []string) string
	BuildUpdateSQL(tableName string, setClauses []string, pkName string) string
	BuildDeleteSQL(tableName, pkName string) string
	BuildCountSQL(tableName string, whereClause string) string
	Rebind(query string) string
}

SQLBuilder defines the contract for SQL generation with dialect-specific identifier quoting.

func NewSQLBuilder

func NewSQLBuilder(d Dialector) SQLBuilder

--- SQLBuilder Factory ---

type Thing

type Thing[T Model] struct {
	// contains filtered or unexported fields
}

Thing is the central access point for ORM operations, analogous to gorm.DB. It holds database/cache clients and the context for operations.

func New

func New[T Model](db DBAdapter, cache CacheClient) (*Thing[T], error)

New creates a new Thing instance with default context.Background(). Accepts one or more CacheClient; if none provided, uses defaultLocalCache.

func Use

func Use[T Model]() (*Thing[T], error)

Use returns a Thing instance for the specified type T, using the globally configured DBAdapter and CacheClient. The package MUST be configured using Configure() before calling Use[T].

func (*Thing[T]) ByID

func (t *Thing[T]) ByID(id int64) (T, error)

ByID fetches a single model by its ID.

func (*Thing[T]) ByIDs

func (t *Thing[T]) ByIDs(ids []int64, preloads ...string) (map[int64]T, error)

ByIDs retrieves multiple records by their primary keys and optionally preloads relations.

func (*Thing[T]) Cache

func (t *Thing[T]) Cache() CacheClient

Cache returns the underlying CacheClient associated with this Thing instance.

func (*Thing[T]) CacheStats

func (t *Thing[T]) CacheStats(ctx context.Context) CacheStats

CacheStats returns cache operation statistics for monitoring and hit/miss analysis.

func (*Thing[T]) ClearCacheByID

func (t *Thing[T]) ClearCacheByID(ctx context.Context, id int64) error

ClearCacheByID removes the cache entry for a specific model instance by its ID. Note: This is now a Thing[T] method.

func (*Thing[T]) DB

func (t *Thing[T]) DB() *sql.DB

DB returns the underlying *sql.DB for advanced/raw SQL use cases.

func (*Thing[T]) Delete

func (t *Thing[T]) Delete(value T) error

Delete performs a hard delete on the record from the database.

func (*Thing[T]) Load

func (t *Thing[T]) Load(model T, relations ...string) error

Load eagerly loads specified relationships for a given model instance.

func (*Thing[T]) Query

func (t *Thing[T]) Query(params QueryParams) (*CachedResult[T], error)

Query prepares a query based on QueryParams and returns a *CachedResult[T] for lazy execution. The actual database query happens when Count() or Fetch() is called on the result. It returns the CachedResult instance and a nil error, assuming basic validation passed. Error handling for query execution is done within CachedResult methods.

func (*Thing[T]) Save

func (t *Thing[T]) Save(value T) error

Save creates or updates a record in the database.

func (*Thing[T]) SoftDelete

func (t *Thing[T]) SoftDelete(value T) error

SoftDelete performs a soft delete on the record by setting the 'deleted' flag to true and updating the 'updated_at' timestamp. It uses saveInternal to persist only these changes.

func (*Thing[T]) ToJSON

func (t *Thing[T]) ToJSON(model T, opts ...JSONOption) ([]byte, error)

ToJSON serializes the provided model instance to JSON based on the given options.

func (*Thing[T]) WithContext

func (t *Thing[T]) WithContext(ctx context.Context) *Thing[T]

WithContext returns a shallow copy of Thing with the context replaced. This is used to set the context for a specific chain of operations.

type Tx

type Tx interface {
	Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error
	Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error
	Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
	Commit() error
	Rollback() error
}

Tx defines the interface for transaction operations.

Directories

Path Synopsis
drivers
examples
01_basic_crud command
Standalone example: Basic CRUD operations with Thing ORM Note: Only one main.go can be run at a time in the examples directory.
Standalone example: Basic CRUD operations with Thing ORM Note: Only one main.go can be run at a time in the examples directory.
04_hooks command
internal

Jump to

Keyboard shortcuts

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