Golar v0.5.1

ORM

database/orm maps Go structs to tables on top of the query builder. It works with *database.DB and *database.Tx.

Models

type User struct {
    orm.Model                 // ID int64 (primary key), CreatedAt, UpdatedAt
    Name    string
    Email   *string           // nullable column
    Profile *Profile `orm:"hasOne"`
    Posts   []Post   `orm:"hasMany"`
    Roles   []Role   `orm:"belongsToMany"`
}

type Post struct {
    orm.Model
    orm.SoftDeletes           // DeletedAt *time.Time
    UserID int64
    Title  string
    Author *User `orm:"belongsTo,foreignKey=user_id"`
}
Rule Default Override
Table plural snake_case type name (User → users, Category → categories) func (User) TableName() string
Column snake_case field name (UserID → user_id) `db:"name"`
Skipped field — `db:"-"`; unexported fields are always skipped
Primary key the id column `db:"code,pk"`

Integer primary keys are generated by the database when zero. created_at and updated_at fields (time.Time, *time.Time or sql.NullTime) are maintained automatically. A nullable deleted_at field (*time.Time or sql.NullTime) enables soft deletes. Embedded structs are flattened; embed them by value.

Use nullable Go types (*string, sql.NullString, *time.Time, …) for columns that may be NULL. MySQL DSNs need parseTime=true to scan time columns.

orm.Register(User{}, Post{}) validates mappings and relationships at startup. Models are otherwise parsed on first use and cached. orm.Describe(User{}) returns the table, primary key, columns and relations.

CRUD

user := User{Name: "Ada"}
err := orm.Create(ctx, db, &user)       // sets user.ID, CreatedAt, UpdatedAt

found, err := orm.Find[User](ctx, db, user.ID)
if errors.Is(err, orm.ErrNotFound) { … } // also matches sql.ErrNoRows

found.Name = "Ada Lovelace"
err = orm.Save(ctx, db, found)          // updates every column except id, created_at, deleted_at

err = orm.Delete(ctx, db, found)        // soft delete if supported, otherwise DELETE

Save, Delete, ForceDelete and Restore return orm.ErrMissingPrimaryKey for a model with a zero primary key.

Queries

users, err := orm.Query[User](db).
    Where("active", "=", true).
    OrderBy("name", "asc").
    Limit(20).
    Get(ctx)                            // []User

first, err := orm.Query[User](db).OrderBy("id", "asc").First(ctx)
n, err := orm.Query[User](db).WhereNull("email").Count(ctx)
page, meta, err := orm.Query[User](db).OrderBy("id", "asc").Paginate(ctx, 2, 25)

Where, WhereNull, WhereNotNull, WhereIn, WhereNotIn, OrderBy, Limit and Offset behave as in the query builder. Builders are immutable.

Bulk writes return the number of affected rows:

n, err := orm.Query[User](db).WhereIn("id", ids).Update(ctx, map[string]any{"active": false})
n, err = orm.Query[Post](db).Where("user_id", "=", id).Delete(ctx)

Update also sets updated_at. Like the query builder, bulk Update, Delete, ForceDelete and Restore refuse to run without a Where clause unless you call AllRows().

Relationships

Tag Field type Default keys Options
hasOne *Related related.<owner>_id = owner.id foreignKey, localKey
hasMany []Related related.<owner>_id = owner.id foreignKey, localKey
belongsTo *Related owner.<field>_id = related.id foreignKey, ownerKey
belongsToMany []Related pivot <a>_<b> (alphabetical singulars, e.g. role_user) with <owner>_id and <related>_id pivot, foreignPivotKey, relatedPivotKey, localKey, relatedKey

Options are written as orm:"hasMany,foreignKey=author_id". <owner> is the snake_case type name, <field> the snake_case field name.

Load relations onto existing models:

err := orm.Load(ctx, db, &user, "Posts", "Profile")
err = orm.LoadAll(ctx, db, users, "Roles")

Eager loading

users, err := orm.Query[User](db).
    With("Profile", "Roles", "Posts.Comments", "Posts.Author").
    Get(ctx)

Each relation at each nesting level runs exactly one query (WHERE key IN (…), chunked in batches of 500 keys), so loading 1 or 1,000 users with their posts and comments always takes three queries. This avoids N+1 queries. Unmatched hasMany/belongsToMany fields are set to empty, non-nil slices; unmatched hasOne/belongsTo fields are nil. Related rows are ordered by primary key, and soft-deleted related rows are excluded.

Soft deletes

Models with a nullable deleted_at field (for example via orm.SoftDeletes) are soft-deleted:

orm.Delete(ctx, db, &post)        // UPDATE … SET deleted_at = now
orm.Restore(ctx, db, &post)       // deleted_at = NULL
orm.ForceDelete(ctx, db, &post)   // DELETE

orm.Query[Post](db).Count(ctx)               // excludes deleted rows
orm.Query[Post](db).WithTrashed().Count(ctx) // includes them
orm.Query[Post](db).OnlyTrashed().Get(ctx)   // only deleted rows

Create the column with table.SoftDeletes() in a migration.

Transactions

Pass a transaction anywhere a connection is accepted:

err := db.TransactionContext(ctx, func(tx *database.Tx) error {
    return orm.Create(ctx, tx, &user)
})

Not yet supported

Model hooks, scopes, factories, seeders, upserts and table aliases in relations.

Edit this page on GitHub