This article details the internal architecture and SQL execution workflow of Gorm, the popular ORM framework for Go. It shares practical techniques for model definition, querying, and updating, while solving common issues like time zone discrepancies, soft deletion, and transactions. It is tailored for advanced Gorm developers.
As the most widely used ORM (Object-Relational Mapping) framework in the Go ecosystem, Gorm significantly simplifies database operations. However, most developers only utilize its basic features and have limited knowledge of its internal logic and advanced techniques. Starting from Gorm’s core principles, this article combines real-world development scenarios to outline its SQL execution workflow, practical functions, and common pitfalls, helping you move from “knowing how to use it” to “mastering it”.

1. Gorm Core Concepts & Architecture
To use Gorm proficiently, you first need to understand its design logic. At its heart, Gorm is a “SQL codification tool” that converts developer method calls into SQL statements and interacts with the database.
1.1 What is ORM?
ORM (Object-Relational Mapping) serves as a bridge between code and databases, with three core functions:
- Mapping database tables to Go structs
- Mapping table columns to struct fields
- Converting struct operations to SQL statements
Its advantages are clear:
- No need to write raw SQL
- Reduces error rates
- Supports multiple databases (MySQL, PostgreSQL, etc.)
However, it also has limitations:
- Auto-generated SQL may not be optimal
- Requires learning framework-specific rules
1.2 Gorm Code Architecture
Gorm uses several core objects to 实现 “method-to-SQL conversion”. Understanding these objects is key to grasping its overall logic:
| Object | Core Role | Key Attributes / Functions |
|---|---|---|
| DB | Database connection instance | Manages connections, stores configuration |
| Config | Stores user settings | Controls plural table names, DryRun mode, prepared statements, etc. |
| Statement | Maps SQL statements | Stores WHERE conditions, SELECT fields, table names, etc. |
| Schema | Maps database table structures | Associates structs with table names and field mappings |
| Field | Maps table column details | Stores column names, data types, primary key/non-null status, etc. |
Gorm’s methods fall into two categories, and the method chain follows a process of “assembling SQL → executing SQL”:
- Process methods: Only assemble SQL (no execution), e.g.,
Where(add conditions),Select(specify fields),Model(bind a struct). - Terminator methods: Execute SQL after assembly and parse results, e.g.,
Find(query),Create(insert),Update(update),Delete(delete).
1.3 Relationship Between trpc-go/gorm and Native Gorm
If your project uses the trpc-go framework, you may encounter the trpc-go/trpc-database/gorm package. It is not a reimplementation of Gorm but a wrapper for native Gorm, with three core advantages:
- Simplifies database connection configuration, eliminating repetitive initialization code.
- Integrates Gorm into trpc-go services, supporting unified framework configuration.
- Provides Polaris dynamic service discovery for flexible database switching.
2. How Does a SQL Statement Execute in Gorm?
Let’s take a common Gorm query code snippet and break down its execution process to understand the full workflow from “method call” to “database response”:
var user User
db := db.Model(user).Select("age", "name").Where("age = ?", 18).Or("name = ?", "tencent").Find(&user)
if err := db.Error; err != nil {log.Printf("Find failed, err: %v", err)
}
2.1 Full Execution Workflow
Preparations
Call gorm.Open() to create a DB object based on the database type (e.g., MySQL) and DSN, then initialize the connection.
SQL Assembly (Process Methods)
Model(user): Informs Gorm to operate on the table associated withuserand updates the table name inStatement.Select("age", "name"): Adds the fields to be queried toStatement.Selects.Where(...)andOr(...): Parses conditions, generatesWHERE age = 18 OR name = 'tencent', and stores it inStatement.Clauses.
SQL Execution (Terminator Method Find)
- Checks
Statementand completes the SQL statement (e.g.,SELECT age, name FROM users WHERE ...). - Calls the database driver’s
QueryContextto send the SQL to the database. - Receives the database response, parses the results, and populates them into
&user. - Stores error information, affected row counts, etc., in the
DBobject and returns it to the developer.
2.2 Key Code Snippets
Taking Select and Where as examples, here’s how Gorm assembles SQL:
// Select method: Adds fields to Statement.Selects
func (db *DB) Select(query interface{}, args ...interface{}) (tx *DB) {tx = db.getInstance()
// Parses incoming fields (e.g., "age" or []string{"age", "name"})
switch v := query.(type) {
case string:
tx.Statement.Selects = append(tx.Statement.Selects, v)
case []string:
tx.Statement.Selects = append(tx.Statement.Selects, v...)
}
return tx
}
// Where method: Adds conditions to Statement.Clauses
func (db *DB) Where(query interface{}, args ...interface{}) (tx *DB) {tx = db.getInstance()
// Parses conditions and generates Clause objects
if conds := tx.Statement.BuildCondition(query, args...); len(conds) > 0 {tx.Statement.AddClause(clause.Where{Exprs: conds})
}
return tx
}
3. Gorm Practical Tips: Filling Knowledge Gaps
Many practical Gorm features are easily overlooked in daily development. Mastering these can significantly improve efficiency.
3.1 Model Definition Tips
Models are the foundation of Gorm’s database interactions. Pay attention to these details:
Controlling Table Name Plurality
Gorm defaults to converting struct names to plural table names (e.g., User → users). To disable this, configure it during initialization:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
NamingStrategy: schema.NamingStrategy{SingularTable: true, // Disable plural table names},
})
Embedding Base Models
Gorm provides a gorm.Model struct that includes ID, CreatedAt, UpdatedAt, and DeletedAt. Embed it in your custom struct to avoid redefining these common fields:
type User struct {
gorm.Model // Embeds the base model, automatically adding 4 common fields
Name string
Age int
}
Note: Embedding DeletedAt automatically enables soft deletion (deletion only updates DeletedAt instead of physically removing data).
Struct Embedding (Embed)
For structs with many fields, split related fields into sub-structs. Use the embedded tag to associate them, and embeddedPrefix to add field prefixes:
// Sub-struct
type Author struct {
Name string `gorm:"column:name"`
Email string `gorm:"column:email"`
}
// Main struct (with embed association)
type Blog struct {
ID int `gorm:"column:id"`
Author Author `gorm:"embedded;embeddedPrefix:author_"` // Fields become author_name, author_email
Upvotes int32 `gorm:"column:upvotes"`
}
3.2 Query Optimization Tips
Queries are high-frequency operations. Choosing the right method reduces performance overhead:
First/Take/Last vs. Find
First/Take/Last: ReturnErrRecordNotFoundif no data is found and automatically addLIMIT 1. Suitable for “single-record queries” (e.g., query by primary key).Find: Does not return an error if no data is found and queries all matching records. Suitable for “multi-record queries” or “primary key/unique key equality queries” (avoids extra error checks).
Simplifying Query Conditions
For simple conditions, skip Where and write conditions directly in Find for cleaner code:
// Equivalent to: db.Where("status = ? and update_time < ?", 1, time.Now()).Find(&user)
db.Find(&user, "status = ? and update_time < ?", 1, time.Now())
Using Pluck for Single-Field Queries
When only one column of data is needed, Pluck is more intuitive than Select + Find:
var ages []int64
// Equivalent to: db.Model(&User{}).Select("age").Find(&ages)
db.Model(&User{}).Pluck("age", &ages)
3.3 Update Pitfalls to Avoid
The most common pitfall in updates is “zero values not being updated”. Remember these two solutions:
Using map to Update Zero Values
Gorm does not update zero values in structs (e.g., false for bool types) by default. Use map[string]interface{} to force updates:
// Incorrect: Active: false is a zero value and will not be updated
db.Model(&user).Updates(User{ID: 111, Name: "hello", Active: false})
// Correct: Use map to force update all fields
db.Model(&user).Updates(map[string]interface{}{"id": 111, "name": "hello", "active": false})
Using Select to Specify Update Fields
If you must use a struct, explicitly specify fields to update via Select:
db.Model(&user).Select("name", "active").Updates(User{ID: 111, Name: "hello", Active: false})
3.4 Safe Testing: The DryRun Feature
If you don’t have a test environment and fear dirty data from SQL errors, enable DryRun mode. Gorm will print SQL without executing it, allowing pre-verification:
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{DryRun: true, // Enable dry-run mode})
// Only prints SQL, no actual data deletion
db.Where("age < ?", 18).Delete(&User{})
4. Solutions to Common Gorm Issues
Most exceptions encountered in development stem from insufficient understanding of Gorm details. Remember these high-frequency issues:
4.1 8-Hour Time Zone Discrepancy? Add loc=Local to DSN
Issue: time.Now() in code returns the current time, but the time stored in the database is 8 hours behind.Cause: Gorm uses UTC time by default, while time.Now() returns Beijing time (UTC+8), leading to an 8-hour difference.Solution: Add loc=Local to the DSN when initializing the DB object to make Gorm use the system time zone:
// DSN format (key: add loc=Local at the end)
dsn := "root:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
4.2 How to Implement Soft Deletion? Embed DeletedAt
Issue: Want to “mark data as deleted without physically removing it”.Solution: Embed gorm.DeletedAt (or directly embed gorm.Model) in the struct. Gorm will handle it automatically:
- On deletion: Executes
UPDATE users SET deleted_at = current_time WHERE ...(no physical deletion). - On query: Automatically adds
WHERE deleted_at IS NULL(excludes deleted data).
To query deleted data, use Unscoped():
// Query all data, including deleted records
db.Unscoped().Find(&users)
4.3 Transactions Are Not “Batched SQL Execution”—They Rely on Native Database Support
Issue: Assuming Gorm transactions “store SQL first and send it all on commit”, but results are returned immediately after Select. Why?Truth: Gorm transactions depend on native database support, with real-time interaction at each step:
tx := db.Begin(): SendsSTART TRANSACTIONto the database.tx.Find(&user): SendsSELECT ...to the database and returns results in real time.tx.Commit(): SendsCOMMITto the database to confirm the transaction.- If an error occurs,
tx.Rollback(): SendsROLLBACKto undo the transaction.
4.4 Bulk Creation & Primary Key Conflict Handling
Bulk Creation
Use CreateInBatches and specify a batch size (to avoid overly long SQL statements):
var users []User
// Assume users contains 100 records
db.CreateInBatches(users, 50) // Insert in 2 batches of 50 records each
Primary Key / Unique Key Conflict Handling
Use clause.OnConflict to specify a conflict strategy. Gorm will generate an ON DUPLICATE KEY UPDATE statement (logic implemented by the database):
// Update all fields on conflict
db.Clauses(clause.OnConflict{UpdateAll: true}).CreateInBatches(&users, 50)
Conclusion
Gorm’s core is “encapsulating SQL logic into Go methods”. Understanding core objects like DB and Statement allows you to master its execution workflow. Practical techniques (e.g., embed, Pluck, DryRun) and pitfall avoidance (time zone discrepancies, zero-value updates) require practice in real-world scenarios.