GORM: The Golang ORM

repository·master·Indexed 13 days ago

https://github.com/go-gorm/gorm

A developer-friendly, full-featured Object-Relational Mapper (ORM) for Golang. GORM provides robust support for complex database operations, associations (Has One, Has Many, Belongs To, Many To Many), lifecycle hooks, eager loading via Preload and Joins, and advanced querying with a SQL Builder. It includes features for transactions, batch inserts, auto migrations, and a flexible plugin API for extensibility.

Tokens
17.4K
Snippets
75
Records
87
Agent score
96%

What's inside GORM

  1. Overview of GORM features

    master

    GORM is a full-featured ORM for Golang designed to be developer-friendly. Key capabilities include:

    • Associations: Supports Has One, Has Many, Belongs To, Many To Many, Polymorphism, and Single-table inheritance.
    • Hooks: Lifecycle callbacks including Before/After Create, Before/After Save, Before/After Update, Before/After Delete, and Before/After Find.
    • Eager Loading: Use Preload and Joins to load associated data.
    • Transactions: Supports standard transactions, nested transactions, Save Points, and RollbackTo functionality.
    • Advanced Querying: Includes a SQL Builder, Upsert, Locking, Optimizer/Index/Comment Hints, and NamedArg. You can perform Search, Update, and Create operations using SQL Expr.
    • Data Management: Supports Batch Insert, FindInBatches, Find To Map, and Auto Migrations.
    • Extensibility: A flexible plugin API allows for features like a Database Resolver (for Multiple Databases or Read/Write Splitting) and Prometheus integration.
  2. Perform complex queries with ChainInterface[T]

    master

    When you call methods like .Where(), .Table(), or .Select() on a generic interface, you transition into a ChainInterface[T]. This interface allows you to build complex SQL queries using a fluent API while maintaining type safety for the eventual execution step.

    Key methods in ChainInterface[T]:

    • Filtering: .Where(query, args...), .Not(query, args...), .Or(query, args...).
    • Pagination/Ordering: .Limit(offset), .Offset(offset), .Order(value).
    • Joins & Preloading: .Joins(...) for SQL joins and .Preload(association, query) for eager loading relationships.
    • Selection: .Select(query, args...), .Omit(columns...).
    • Execution: Once the chain is built, you use ExecInterface[T] methods like .Find(), .First(), or .Scan() to execute the query and retrieve results.
    // Example of chaining for a complex query
    var users []User
    err := gorm.G[User](db). 
        Where("age > ?", 21). 
        Order("name desc"). 
        Limit(10). 
        Find(ctx, &users)
  3. How GORM callbacks work and their execution order

    master

    GORM's callback system is managed by processors that hold a collection of callback objects. When a database operation is executed, GORM compiles these callbacks into a sorted list of functions (fns) and executes them sequentially.

    Execution Order Control

    You can control where your callback sits in the execution chain using these modifiers:

    • Before(name string): Executes your callback before the named callback. Use "*" to indicate it should be at the very beginning of the chain.
    • After(name string): Executes your callback after the named callback. Use "*" to indicate it should be at the very end of the chain.
    • Match(fc func(*DB) bool): A conditional hook. The callback is only added to the execution chain if the match function returns true for the specific *DB instance being processed.

    Callback Lifecycle

    1. Registration: A callback is added to the processor's list.
    2. Compilation: The processor sorts the callbacks based on Before/After constraints and filters out those marked for removal.
    3. Execution: During the database operation, the compiled functions are called one by one, passing the current *DB instance.
    // Registering a conditional callback
    db.Callback().Query().Match(func(d *gorm.DB) bool {
        // Only run this callback if we are querying the 'users' table
        return d.Statement.Table == "users"
    }).Register("conditional_query_log", func(d *gorm.DB) {
        fmt.Println("Querying users table...")
    })
  4. Use Unscoped to bypass Soft Delete filters

    master

    By default, when a model uses gorm.DeletedAt, all queries (Find, First, etc.) will automatically include a WHERE deleted_at IS NULL clause to exclude soft-deleted records.

    To include soft-deleted records in your queries or to perform a permanent hard delete, use the .Unscoped() method on the GORM statement.

    // Find all users, including soft-deleted ones
    db.Unscoped().Find(&users)
    
    // Permanently delete a record (hard delete)
    db.Unscoped().Delete(&user)
  5. Supported destination types for Scan()

    master

    The Scan function automatically detects the destination type (db.Statement.Dest) and applies the appropriate scanning logic:

    • map[string]interface{} or *map[string]interface{}: Scans columns into map keys. Handles driver.Valuer and sql.RawBytes (converting them to strings).
    • *[]map[string]interface{}: Scans multiple rows into a slice of maps.
    • Primitive Types: Supports int, uint, float, bool, string, time.Time, and sql.Null* types for single-value scans.
    • Structs/Pointers: Scans columns into struct fields using the model's schema. Supports nested relations if joins are present.
    • Slices/Arrays: Appends new elements to the slice or updates existing elements if ScanUpdate is used.
  6. Understand the Statement abstraction

    master

    The Statement struct is the core engine of GORM's SQL generation. It maintains the state of a single database operation, including the model being operated on, the table name, the destination for results, and a collection of Clauses (like WHERE, ORDER BY, LIMIT) that define the SQL structure.

    Key components of a Statement include:

    • Clauses: A map of SQL clauses that are built into a final query.
    • Schema: The parsed metadata of the model being used.
    • SQL: A strings.Builder used to accumulate the generated SQL string.
    • Vars: A slice of arguments used for prepared statement placeholders.
    • Dest: The target object where query results are scanned.

    Developers typically interact with Statement indirectly through the *gorm.DB instance, which manages the lifecycle of statements during callbacks.

  7. Register and manage GORM callbacks

    master

    GORM uses a callback system to allow developers to hook into the lifecycle of database operations. You can register custom functions that execute before or after specific database actions like Create, Query, Update, Delete, Row, or Raw.

    Callback Processors

    Each major operation has its own processor. You access them via the following methods on the *gorm.DB instance:

    • Create()
    • Query()
    • Update()
    • Delete()
    • Row()
    • Raw()

    Registering a Callback

    To register a callback, use the Register method on a processor. You can chain Before or After to control the execution order relative to existing callbacks.

    Managing Callbacks

    • Replace(name string, fn func(*DB)): Replaces an existing callback with a new one.
    • Remove(name string): Removes an existing callback by name.
    • Match(fc func(*DB) bool): Allows you to register a callback that only executes if the provided predicate function returns true for the current *DB instance.
    // Example: Registering a 'before create' callback
    db.Callback().Create().Before("gorm:create").Register("my_custom_callback", func(d *gorm.DB) {
        fmt.Println("About to create record...")
    })
  8. Configure GORM via Config struct

    master

    The Config struct allows you to customize GORM's behavior globally. Key fields include:

    • SkipDefaultTransaction: If true, GORM will not perform single create/update/delete operations in a transaction by default.
    • NamingStrategy: Customizes table and column naming.
    • Logger: Customizes the logging interface.
    • PrepareStmt: Enables prepared statement caching for performance.
    • AllowGlobalUpdate: Allows updates without a Where clause (dangerous, use with caution).
    • TranslateError: Enables error translation via the Dialector.
  9. Parse database indexes from a Schema

    master

    The ParseIndexes method on a *Schema object scans all fields within the schema to identify and construct database index definitions based on struct tags. It looks for INDEX or UNIQUEINDEX settings within the gorm tag.

    When multiple fields share the same index name, they are grouped into a single Index object, allowing for the creation of composite indexes. The resulting []*Index slice contains all identified indexes, with fields within each index sorted by their Priority.

    // Assuming `schema` is an existing *Schema instance
    indexes := schema.ParseIndexes()
    for _, idx := range indexes {
    	fmt.Printf("Index Name: %s, Class: %s\n", idx.Name, idx.Class)
    	for _, opt := range idx.Fields {
    		fmt.Printf("  - Field: %s\n", opt.Field.Name)
    	}
    }
  10. Handle transactions with PreparedStmtTX

    master

    When starting a transaction on a PreparedStmtDB using BeginTx, you receive a PreparedStmtTX. This type ensures that prepared statements used within the transaction are correctly scoped to that transaction.

    Key Behaviors:

    • BeginTx(ctx, opt): Starts a transaction. Returns a PreparedStmtTX which wraps both the transaction and the parent PreparedStmtDB.
    • Commit(): Commits the transaction.
    • Rollback(): Rolls back the transaction.
    • ExecContext, QueryContext, QueryRowContext: These methods within the transaction use tx.StmtContext to ensure the cached statement is executed within the transaction's context.
    ctx := context.Background()
    
    // Start a transaction
    presStmtTx, err := db.BeginTx(ctx, nil)
    if err != nil {
        panic(err)
    }
    
    // Use the transaction
    err = presStmtTx.QueryRowContext(ctx, "SELECT name FROM users WHERE id = ?", 1).Scan(&name)
    if err != nil {
        presStmtTx.Rollback()
    }
    
    // Commit the transaction
    err = presStmtTx.Commit()
  11. Manage database indexes with Migrator

    master

    The Migrator interface allows for manual index control:

    • CreateIndex(dst interface{}, name string) error: Creates an index.
    • DropIndex(dst interface{}, name string) error: Drops an index.
    • HasIndex(dst interface{}, name string) bool: Checks if an index exists.
    • RenameIndex(dst interface{}, oldName, newName string) error: Renames an index.
    • GetIndexes(dst interface{}) ([]Index, error): Returns a list of Index objects for the model.