thing

package module
v0.1.26 Latest Latest
Warning

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

Go to latest
Published: Jan 7, 2026 License: MIT Imports: 22 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,default:18"`
}

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)

	// Chainable Query API
	usersOver18, err := users.Where("age > ?", 18).Order("name ASC").Fetch(0, 100)
	if err != nil { /* handle error */ }
	fmt.Println(usersOver18)

	// 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 == "[email protected]" {
			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: "[email protected]"}
	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: "[email protected]"}
	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)

Advanced Usage

Raw SQL Execution

While Thing ORM focuses on abstracting common database interactions, there are times when you need to execute raw SQL queries for complex operations, database-specific features, or bulk updates/deletions not directly covered by the ORM's primary API.

Thing ORM provides two main ways to execute raw SQL:

  1. Using DBAdapter Methods (Recommended for most cases): The DBAdapter interface (accessible via thingInstance.DBAdapter() or thing.GlobalDB() if globally configured) provides convenient methods for raw SQL execution that are integrated with Thing ORM's error handling and type scanning:

    • Exec(ctx context.Context, query string, args ...interface{}) (sql.Result, error): For SQL statements that don't return rows (e.g., INSERT, UPDATE, DELETE, DDL statements).
    • Get(ctx context.Context, dest interface{}, query string, args ...interface{}) error: For queries that return a single row. dest should be a pointer to a struct or a primitive type slice. Thing ORM will scan the row into dest.
    • Select(ctx context.Context, dest interface{}, query string, args ...interface{}) error: For queries that return multiple rows. dest should be a pointer to a slice of structs or a slice of primitive types.
    package main
    
    import (
    	"context"
    	"fmt"
    	"log"
    
    	"github.com/burugo/thing"
    	// Assume User model and dbAdapter are set up
    )
    
    func main() {
    	// Assume userThing is an initialized *thing.Thing[*User] instance
    	// or thing.Configure() has been called.
    
        var dbAdapter thing.DBAdapter
        // Get the adapter, e.g., from a Thing instance or global config
        userThing, err := thing.Use[*User]() // if globally configured
        if err != nil {
            log.Fatal(err)
        }
        dbAdapter = userThing.DBAdapter()
    
    	ctx := context.Background()
    
    	// Example: Exec for an UPDATE statement
    	result, err := dbAdapter.Exec(ctx, "UPDATE users SET age = ? WHERE name = ?", 35, "Alice")
    	if err != nil {
    		log.Fatal("Raw Exec failed:", err)
    	}
    	rowsAffected, _ := result.RowsAffected()
    	fmt.Printf("Raw Exec updated %d rows\n", rowsAffected)
    
    	// Example: Get for a single row
    	type UserAgeStats struct {
    		AverageAge float64 `db:"average_age"`
    		MaxAge     int       `db:"max_age"`
    	}
    	var stats UserAgeStats
    	err = dbAdapter.Get(ctx, &stats, "SELECT AVG(age) as average_age, MAX(age) as max_age FROM users")
    	if err != nil {
    		log.Fatal("Raw Get failed:", err)
    	}
    	fmt.Printf("User Stats: Average Age: %.2f, Max Age: %d\n", stats.AverageAge, stats.MaxAge)
    
    	// Example: Select for multiple rows (scanning into a slice of structs)
    	var activeUsers []*User // Assuming User struct is defined elsewhere
    	err = dbAdapter.Select(ctx, &activeUsers, "SELECT id, name, age FROM users WHERE age > ?", 30)
    	if err != nil {
    		log.Fatal("Raw Select failed:", err)
    	}
    	fmt.Printf("Found %d active users over 30 via raw SQL:\n", len(activeUsers))
    	for _, u := range activeUsers {
    		fmt.Printf("- ID: %d, Name: %s, Age: %d\n", u.ID, u.Name, u.Age)
    	}
    }
    
  2. Accessing the Underlying *sql.DB (For Advanced Use): If you need even lower-level control or want to use features of the standard database/sql package directly (like transactions not managed by DBAdapter), you can get the raw *sql.DB object:

    • From a thing.Thing[T] instance: sqlDB := thingInstance.DB()
    • From a thing.DBAdapter instance: sqlDB := dbAdapter.DB()

    Once you have the *sql.DB object, you can use its methods like Prepare, QueryRow, Query, Exec, BeginTx, etc., as you normally would with the standard library.

    // ... (imports and setup as above)
    
    func main() {
        // Assume userThing is an initialized *thing.Thing[*User] instance
        userThing, err := thing.Use[*User]()
        if err != nil {
            log.Fatal(err)
        }
        sqlDB := userThing.DB() // Get the *sql.DB object
        ctx := context.Background()
    
        // Example: Using standard library's QueryRow
        var totalUsers int
        err = sqlDB.QueryRowContext(ctx, "SELECT COUNT(*) FROM users").Scan(&totalUsers)
        if err != nil {
            log.Fatal("sql.DB QueryRowContext failed:", err)
        }
        fmt.Printf("Total users from sql.DB: %d\n", totalUsers)
    
        // Remember to handle errors and resources (like sql.Rows) appropriately.
    }
    

Choosing the Right Method:

  • For most common raw SQL needs, including transaction management, using the Exec, Get, Select, and BeginTx (followed by Commit or Rollback on the returned thing.Tx object) methods on the DBAdapter is recommended. These methods are integrated with Thing ORM's type scanning and error handling.
  • Accessing the underlying *sql.DB object is appropriate if you need to use very specific features of the standard database/sql package that are not directly exposed or wrapped by the DBAdapter interface, or if you require an even lower level of control over database interactions than what DBAdapter provides.

Schema/Migration Tools

Usage Overview

Thing ORM provides a powerful migration tool that can create, modify, and drop tables, columns, indexes, and constraints based on your model structs.

Index Declaration

You can declare indexes on your model structs using struct tags. Thing ORM will automatically create and manage these indexes in your database.

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,unique"` // Will create a unique index on email
	Age   int    `db:"age"`
}

type Post struct {
	thing.BaseModel
	UserID  uint   `db:"user_id,index"` // Will create an index on user_id
	Title   string `db:"title"`
	Content string `db:"content"`
}
Auto Migration Example
package main

import (
	"log"

	"github.com/burugo/thing"
	"github.com/burugo/thing/drivers/db/sqlite" // Or any other supported driver
)

type User struct {
	thing.BaseModel
	Name  string `db:"name"`
	Email string `db:"email,unique"` // Will create a unique index on email
	Age   int    `db:"age"`
}

type Post struct {
	thing.BaseModel
	UserID  uint   `db:"user_id,index"` // Will create an index on user_id
	Title   string `db:"title"`
	Content string `db:"content"`
}

func main() {
	// 1. Setup DB Adapter (e.g., SQLite)
	db, err := sqlite.NewSQLiteAdapter(":memory:")
	if err != nil {
		log.Fatal("Failed to connect to database:", err)
	}
	defer db.Close()

	// 2. Configure Thing ORM globally (optional, can also use thing.New per model)
	if err := thing.Configure(db, nil); err != nil { // Using nil for default in-memory cache
		log.Fatal("Failed to configure Thing ORM:", err)
	}

	// 3. Run AutoMigrate for your models
	// This will create tables if they don't exist, or add/modify columns/indexes.
	// By default, it will NOT drop columns from existing tables unless `thing.AllowDropColumn` is true.
	// thing.AllowDropColumn = true // Uncomment to allow dropping columns
	if err := thing.AutoMigrate(&User{}, &Post{}); err != nil {
		log.Fatal("AutoMigrate failed:", err)
	}

	log.Println("Migration successful!")
}
Controlling Column Deletion

By default, thing.AutoMigrate will add new columns and modify existing ones to match your model structs, but it will not automatically delete columns from your database tables if they are removed from your model structs. This is a safety measure to prevent accidental data loss.

To enable automatic column deletion during migration, you can set the global boolean variable thing.AllowDropColumn to true before calling thing.AutoMigrate:

// Enable automatic column deletion
thing.AllowDropColumn = true

// Now, if a model struct is updated to remove a field (and its `db` tag),
// AutoMigrate will attempt to generate and execute a `DROP COLUMN` statement.
if err := thing.AutoMigrate(&MyModelWithRemovedField{}); err != nil {
    log.Fatal("AutoMigrate failed:", err)
}

// It's good practice to reset it to false after migrations if needed elsewhere.
thing.AllowDropColumn = false

Caution: Enabling thing.AllowDropColumn can lead to data loss if columns are removed inadvertently. Use this feature with care, especially in production environments. It's often safer to manage column deletions manually via dedicated migration scripts for more control.

Multi-Database Testing

Thing ORM is designed to work with multiple databases, allowing you to seamlessly switch between MySQL, PostgreSQL, and SQLite with a unified API and automatic SQL dialect adaptation.

FAQ

Contributing

Performance

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

View Source
var AllowDropColumn = false

AllowDropColumn controls whether AutoMigrate will execute 'DROP COLUMN' statements. Defaults to false (columns will not be dropped).

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. Includes cache version to isolate keys across application restarts.

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 GetCacheVersion added in v0.1.25

func GetCacheVersion() int64

GetCacheVersion returns the current cache version for query cache key generation. This version changes on each application restart, ensuring stale cache keys are ignored.

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{}, fieldsToCache []string, 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

	Incr(ctx context.Context, key string) (int64, error)
	Expire(ctx context.Context, key string, expiration time.Duration) 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.

func Cache added in v0.1.17

func Cache() CacheClient

Cache returns the underlying CacheClient associated with this Thing instance.

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 {
	Err error // New: holds error if Query failed to initialize
	// 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]) Order added in v0.1.2

func (cr *CachedResult[T]) Order(order string) *CachedResult[T]

Order on CachedResult: returns a new instance with updated Order

func (*CachedResult[T]) Preload added in v0.1.2

func (cr *CachedResult[T]) Preload(preloads ...string) *CachedResult[T]

Preload on CachedResult: returns a new instance with updated Preloads

func (*CachedResult[T]) Where added in v0.1.2

func (cr *CachedResult[T]) Where(where string, args ...interface{}) *CachedResult[T]

Where on CachedResult: returns a new instance with updated Where/Args

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 ChannelLock added in v0.1.19

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

ChannelLock implements a mutex using channel semantics

func NewChannelLock added in v0.1.19

func NewChannelLock() *ChannelLock

NewChannelLock creates a new channel-based lock

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 (now supports nested DSL via WithFields logic).

func Include

func Include(fields ...string) JSONOption

Include specifies fields to include in the JSON output (now supports nested DSL via WithFields logic).

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
	NestedRules    map[string]*JSONOptions // Stores nested rules for any field, regardless of include/exclude status
}

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]) All added in v0.1.3

func (t *Thing[T]) All() ([]T, error)

All is a convenience method to query and fetch all records matching the default QueryParams. It's equivalent to calling thingInstance.Query(QueryParams{}).All().

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]) 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]) Order added in v0.1.2

func (t *Thing[T]) Order(order string) *CachedResult[T]

Order on Thing: starts a new query chain

func (*Thing[T]) Preload added in v0.1.2

func (t *Thing[T]) Preload(preloads ...string) *CachedResult[T]

Preload on Thing: starts a new query chain

func (*Thing[T]) Query

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

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. 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 interface{}, opts ...JSONOption) ([]byte, error)

ToJSON serializes the provided model instance (single or collection) to JSON based on the given options.

func (*Thing[T]) Where added in v0.1.2

func (t *Thing[T]) Where(where string, args ...interface{}) *CachedResult[T]

Where on Thing: starts a new query chain

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