| name | gorm-dao |
| description | Write GORM data access code using the DAO (Data Access Object) pattern. Use when creating database models, writing queries, setting up GORM, adding CRUD methods, or working with gorm.io in Go services. Also use when the user mentions "DAO", "data access", "ORM", "database models", "GORM", or is building a Go service that talks to a relational database. |
GORM with the DAO Pattern
This skill guides writing Go data access layers using GORM (gorm.io) organized around the DAO pattern. All database operations go through a single DAO struct, with methods grouped into per-entity files. This keeps callers free from direct database concerns while keeping each file focused.
When to read reference files
This skill includes detailed GORM reference docs in references/. Read them when you need specifics:
references/gorm-basics.md -- Models, struct tags, conventions, CRUD operations, connecting to databases. Read when defining new models or writing basic queries.
references/gorm-associations.md -- Belongs-to, has-one, has-many, many-to-many, preloading, association mode. Read when defining relationships between models or loading related data.
references/gorm-advanced.md -- Scopes, transactions, hooks, migrations, performance tuning, error handling, raw SQL, custom data types. Read when writing complex queries, transactions, or optimizing performance.
references/sqlite-wasm.md -- The ncruces/go-sqlite3 pure-WASM driver and its gormlite dialect. Read when working with SQLite in Go without CGO. This driver is optional -- not every project needs it.
Database choice
Services in this repo generally use PostgreSQL for production databases. The mi service is an exception that uses SQLite because it is a small, self-contained personal service where an embedded database makes sense. When creating a new service, default to Postgres unless there is a specific reason to use SQLite (single-user, embedded, no external DB dependency needed).
The DAO pattern
The DAO struct owns the *gorm.DB connection and exposes domain-specific methods. This keeps database logic contained -- callers never construct raw queries.
Structure
package models
import (
"context"
"errors"
"gorm.io/gorm"
)
var ErrNotFound = errors.New("models: not found")
type DAO struct {
db *gorm.DB
}
func New(dialector gorm.Dialector) (*DAO, error) {
db, err := gorm.Open(dialector, &gorm.Config{})
if err != nil {
return nil, err
}
if err := db.AutoMigrate(&User{}, &Order{}, &Product{}); err != nil {
return nil, err
}
return &DAO{db: db}, nil
}
func (d *DAO) DB() *gorm.DB {
return d.db
}
Key principles
-
Every method takes context.Context as its first parameter and applies it with .WithContext(ctx). This enables cancellation, timeouts, and tracing to flow through.
-
Methods are named for what they do in domain terms, not SQL terms. ActiveSubscription() instead of SelectLatestSubscription(). HasProduct() instead of CountProductsBySKU().
-
Return domain errors, not raw GORM errors, when it makes the caller's life easier. Wrap or translate gorm.ErrRecordNotFound into your own sentinel error if it is a normal case (not exceptional).
-
The DAO constructor runs AutoMigrate. This keeps schema in sync with struct definitions automatically. For production services with complex migration needs, consider a dedicated migration tool instead.
-
Expose DB() for escape hatches but prefer adding DAO methods. Direct DB access should be rare.
-
Group DAO methods by entity file. Methods that operate on a model belong in that model's file, not in dao.go. For example, CreateUser and ListUsers go in user.go alongside the User struct. dao.go holds only the DAO struct, constructor, DB(), Ping(), and other infrastructure that is not tied to a specific entity. This keeps each file focused and easy to navigate as the number of models grows.
Model definition
type User struct {
ID int `gorm:"primaryKey"`
Name string `gorm:"uniqueIndex"`
Email string
Bio *string
}
type Order struct {
gorm.Model
ID string `gorm:"uniqueIndex"`
Total int
UserID int
User User `gorm:"foreignKey:UserID"`
}
When to embed gorm.Model
Embed gorm.Model when you want automatic CreatedAt, UpdatedAt, and soft-delete (DeletedAt) tracking. Skip it when the model is simple and does not need those fields -- just define ID yourself.
ID strategies
- Auto-increment integers: Default for
uint/int primary keys. Simple, good for most cases.
- ULIDs or UUIDs as strings: Use
gorm:"uniqueIndex" on a string ID field. Generate in a BeforeCreate hook or in the DAO method. ULIDs sort chronologically, which is useful for time-ordered data.
Struct tags quick reference
| Tag | Purpose |
|---|
gorm:"primaryKey" | Explicit primary key |
gorm:"uniqueIndex" | Unique index (enforces uniqueness at DB level) |
gorm:"index" | Regular index for WHERE/ORDER performance |
gorm:"foreignKey:FieldName" | Explicit foreign key for associations |
gorm:"column:col_name" | Override column name |
gorm:"type:varchar(100)" | Override column type |
gorm:"default:value" | Default value |
gorm:"not null" | NOT NULL constraint |
gorm:"-" | Ignore field entirely |
See references/gorm-basics.md for the full tag reference.
File organization
models/
dao.go -- DAO struct, New(), DB(), Ping(), infrastructure
user.go -- User model + DAO methods: CreateUser(), GetUser(), ListUsers()
order.go -- Order model + DAO methods: CreateOrder(), GetOrder(), UserOrders()
product.go -- Product model + DAO methods: GetProduct(), ListProducts(), HasProduct()
Each entity file contains both the struct definition and every DAO method that operates on that entity. dao.go is kept lean -- just the connection, constructor, and helpers that do not belong to any single entity.
Query patterns
Always use context
func (d *DAO) GetUser(ctx context.Context, id int) (*User, error) {
var user User
if err := d.db.WithContext(ctx).First(&user, id).Error; err != nil {
return nil, err
}
return &user, nil
}
Loading associations
Use .Joins() for eager loading in a single query (belongs-to/has-one):
func (d *DAO) GetOrder(ctx context.Context, id string) (*Order, error) {
var order Order
err := d.db.WithContext(ctx).
Joins("User").
Where("orders.id = ?", id).
First(&order).Error
return &order, err
}
Use .Preload() for has-many or when you need separate queries:
func (d *DAO) GetUserWithOrders(ctx context.Context, id int) (*User, error) {
var user User
err := d.db.WithContext(ctx).
Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC")
}).
First(&user, id).Error
return &user, err
}
Pagination
func (d *DAO) ListOrders(ctx context.Context, count, page int) ([]Order, error) {
var orders []Order
err := d.db.WithContext(ctx).
Joins("User").
Order("created_at DESC").
Limit(count).
Offset(count * page).
Find(&orders).Error
return orders, err
}
Existence checks
func (d *DAO) HasProduct(ctx context.Context, sku string) (bool, error) {
var count int64
err := d.db.WithContext(ctx).
Model(&Product{}).
Where("sku = ?", sku).
Count(&count).Error
return count > 0, err
}
Transactions
Use transactions when multiple operations must succeed or fail together:
func (d *DAO) TransferOrder(ctx context.Context, orderID string, newUserID int) error {
tx := d.db.Begin()
var order Order
if err := tx.WithContext(ctx).
Where("id = ?", orderID).
First(&order).Error; err != nil {
tx.Rollback()
return err
}
order.UserID = newUserID
if err := tx.WithContext(ctx).Save(&order).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Commit().Error; err != nil {
return err
}
return nil
}
For simpler cases, use the closure form which auto-commits/rollbacks:
err := d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&record1).Error; err != nil {
return err
}
if err := tx.Create(&record2).Error; err != nil {
return err
}
return nil
})
Observability
Set up structured logging and metrics in the constructor:
import (
slogGorm "github.com/orandin/slog-gorm"
gormPrometheus "gorm.io/plugin/prometheus"
)
func New(dialector gorm.Dialector) (*DAO, error) {
db, err := gorm.Open(dialector, &gorm.Config{
Logger: slogGorm.New(
slogGorm.WithErrorField("err"),
slogGorm.WithRecordNotFoundError(),
),
})
if err != nil {
return nil, err
}
db.Use(gormPrometheus.New(gormPrometheus.Config{
DBName: "myservice",
}))
}
Protobuf conversion
When models need to be served over gRPC or serialized to protobuf, add AsProto() methods to models rather than mixing protobuf tags into GORM structs. This keeps the database layer clean:
func (u *User) AsProto() *pb.User {
return &pb.User{
Id: int64(u.ID),
Name: u.Name,
Email: u.Email,
}
}
Connecting to databases
PostgreSQL (default for services)
import "gorm.io/driver/postgres"
dsn := "host=localhost user=myapp password=secret dbname=myapp port=5432 sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
SQLite (for embedded/single-user services)
With the standard CGO driver:
import "gorm.io/driver/sqlite"
db, err := gorm.Open(sqlite.Open("myapp.db"), &gorm.Config{})
With the pure-WASM driver (no CGO required -- see references/sqlite-wasm.md):
import "github.com/ncruces/go-sqlite3/gormlite"
db, err := gorm.Open(gormlite.Open("myapp.db"), &gorm.Config{})
Testing
DAO methods are straightforward to test with a real database. Use SQLite in-memory for fast tests even if production uses Postgres:
func setupTestDAO(t *testing.T) *DAO {
t.Helper()
dao, err := New(sqlite.Open(":memory:"))
if err != nil {
t.Fatal(err)
}
return dao
}
Use table-driven tests for query methods. See the go-table-driven-tests skill for patterns.