Go GORM — ORM for Go with GORM Library and Database Migrations
In this tutorial, you will learn about Go GORM. We cover key concepts, practical examples, and best practices to help you master this topic.
Go GORM provides an ORM with automatic migrations, association management, query building, and hooks for PostgreSQL, MySQL, and SQLite.
What You'll Learn
- Model definition and tags
- Auto Migration
- CRUD operations
- Associations and hooks
Why It Matters
GORM speeds up database development. DodaZIP uses GORM for file processing metadata. Many Go web applications use GORM for rapid prototyping.
Real-World Use
Web application backends, content management systems, e-commerce platforms, data administration tools.
flowchart LR
A["GORM"] --> B["Models"]
A --> C["AutoMigrate"]
A --> D["CRUD"]
A --> E["Associations"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Model Definition
type User struct {
ID uint `gorm:"primarykey"`
Name string `gorm:"size:100;not null"`
Email string `gorm:"uniqueIndex;size:255"`
Age int `gorm:"default:18"`
Active bool `gorm:"default:true"`
Profile Profile `gorm:"foreignKey:UserID"`
Orders []Order `gorm:"foreignKey:UserID"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
Connecting and Migration
func main() {
dsn := "host=localhost user=postgres password=pass dbname=app port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
log.Fatal(err)
}
db.AutoMigrate(&User{}, &Profile{}, &Order{})
}
Create Operations
func main() {
user := User{Name: "Alice", Email: "alice@example.com", Age: 30}
result := db.Create(&user)
fmt.Println("ID:", user.ID)
fmt.Println("Rows affected:", result.RowsAffected)
fmt.Println("Error:", result.Error)
// Batch insert
users := []User{
{Name: "Bob", Email: "bob@example.com"},
{Name: "Charlie", Email: "charlie@example.com"},
}
db.Create(&users)
}
Query Operations
// Find by ID
var user User
db.First(&user, 1)
db.First(&user, "id = ?", 1)
// Find with conditions
db.Where("name = ?", "Alice").First(&user)
db.Where("age > ?", 25).Find(&users)
db.Where("name LIKE ?", "%A%").Find(&users)
// Order, Limit, Offset
db.Order("age desc").Limit(10).Offset(0).Find(&users)
// Select specific fields
db.Select("name", "email").Find(&users)
// Count
var count int64
db.Model(&User{}).Where("active = ?", true).Count(&count)
Update Operations
// Update single field
db.Model(&user).Update("name", "Alice Smith")
// Update multiple fields
db.Model(&user).Updates(User{Name: "Alice", Age: 31})
// Update with map
db.Model(&user).Updates(map[string]interface{}{
"name": "Alice",
"age": 31,
})
// Update with conditions
db.Model(&User{}).Where("active = ?", false).Update("active", true)
Delete Operations
// Soft delete (requires DeletedAt field)
db.Delete(&user, 1)
// Hard delete
db.Unscoped().Delete(&user)
// Delete with conditions
db.Where("active = ?", false).Delete(&User{})
Associations
type Profile struct {
ID uint `gorm:"primarykey"`
UserID uint
Bio string
Avatar string
}
type Order struct {
ID uint `gorm:"primarykey"`
UserID uint
Product string
Amount float64
}
func main() {
// Preload associations
var users []User
db.Preload("Profile").Preload("Orders").Find(&users)
// Nested preloading
db.Preload("Orders.Items").Find(&users)
// Joins
db.Joins("Profile").Find(&users)
}
Hooks
type User struct {
ID uint
Password string
// ...
}
func (u *User) BeforeCreate(tx *gorm.DB) error {
hashed, _ := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
u.Password = string(hashed)
return nil
}
func (u *User) AfterCreate(tx *gorm.DB) error {
tx.Model(u).Update("name", "User: "+u.Name)
return nil
}
Common Mistakes
1. Not Handling Duplicate Errors
result := db.Create(&user)
if result.Error != nil {
// Handle unique constraint violations
if strings.Contains(result.Error.Error(), "duplicate key") { /* handle */ }
}
2. N+1 Query Problem
// Bad: Accessing relations without preloading causes N+1
for _, u := range users {
fmt.Println(u.Profile.Bio) // Executes a query each iteration
}
// Good: db.Preload("Profile").Find(&users)
3. Not Using Transactions
Use db.Transaction for operations that modify multiple records.
4. Ignoring Soft Delete
GORM soft deletes by default. Use Unscoped() for hard deletes.
5. Missing Indexes
Add gorm:"index" to frequently queried fields for performance.
Practice Questions
1. What does AutoMigrate do? Creates/updates database tables to match model structs. Does not delete columns or data.
2. How do you prevent SQL Injection with GORM? GORM uses parameterized queries internally. Use ? placeholders in Where clauses.
3. What is the difference between First and Find? First returns one record, adds ORDER BY id LIMIT 1. Find returns all matching records.
4. How do you implement pagination? Use Scopes: db.Scopes(Paginate(page, pageSize)).Find(&users). Or manually: Limit/Offset.
Challenge: Implement a paginated query scope in GORM.
Solution
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page <= 0 { page = 1 }
if pageSize <= 0 { pageSize = 10 }
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
// Usage
db.Scopes(Paginate(1, 20)).Find(&users)
FAQ
{{< faq question="Should I use GORM or raw SQL?" >}} GORM for development speed, associations, and migrations. Raw SQL for complex queries and performance-critical paths. You can use both in the same project. {{< /faq >}}
{{< faq question="How does GORM handle migrations?" >}} AutoMigrate is safe for development. For production, use versioned migrations with golang-migrate or goose. {{< /faq >}}
{{< faq question="Can I use GORM with transactions?" >}}
Yes. db.Transaction(func(tx *gorm.DB) error { ... }) handles commit and rollback automatically.
{{< /faq >}}
{{< faq question="What is the difference between gorm.Model and custom struct?" >}} gorm.Model includes ID, CreatedAt, UpdatedAt, DeletedAt. Custom structs give you full control over fields and tags. {{< /faq >}}
{{< faq question="How do I log GORM queries?" >}}
Configure logger in gorm.Config: gorm.Config{Logger: logger.Default.LogMode(logger.Info)}. Levels: Silent, Error, Warn, Info.
{{< /faq >}}
Try It Yourself
package main
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"fmt"
)
type Product struct {
ID uint `gorm:"primarykey"`
Name string
Price float64
}
func main() {
db, _ := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
db.AutoMigrate(&Product{})
db.Create(&Product{Name: "Widget", Price: 9.99})
var p Product
db.First(&p, 1)
fmt.Printf("%s: $%.2f\n", p.Name, p.Price)
}
Expected output:
Widget: $9.99
What's Next
Now that you understand GORM, explore Go generics for type-safe reusable code.
| Topic | Description | Link |
|---|---|---|
| Go Generics | Type parameters | {{< ref "31-generics" >}} |
| Go Database SQL | Raw SQL operations | {{< ref "29-database-sql" >}} |
| Go HTTP Servers | Building web servers | {{< ref "27-http-server" >}} |