Apply
Compose model-query extensions without creating a separate query API per extension.
Apply is Oro’s model-query extension point. It lets an extension add query filters, select hidden columns, inject write values, transform loaded models, or run post-write work while the query still uses the normal ModelQuery chain.
products, err := db.Use[Product]().
Apply(translation.Locale("zh-CN")).
Where("Status", "online").
OrderByDesc("ID").
Get(ctx)
children, err := db.Use[Category]().
Apply(nestedset.DescendantsOf(root), nestedset.DefaultOrder()).
Where("Status", "enabled").
Get(ctx)
The important rule is simple: extension packages provide behavior objects, but Where, With, OrderBy, Paginate, Count, Create, Update, and other query methods still come from Oro.
When to use Apply
Use Apply for behavior that belongs to one query chain:
| Use case | Example |
|---|---|
| Add model conditions | nestedset.DescendantsOf(root) |
| Add ordering | nestedset.DefaultOrder() |
| Select internal hidden columns | translation.Locale("zh-CN") |
| Add translated-field conditions | translation.WhereTrans("Name", "Apple") |
| Add write-side data | translation.Write(...) |
| Run cleanup after a write | logroll.Roll(logroll.KeepLast(100000)) |
Use Config.Extensions instead when the behavior must apply globally to all matching queries and writes, such as tenant isolation, audit logging, metrics, or connection routing.
Public interface
Any value implementing oro.Apply can be passed to ModelQuery.Apply.
type Apply interface {
ApplyOro(*ApplyContext) error
}
type ApplyFinalizer interface {
AfterApplyOro(*ApplyContext) error
}
ApplyOro is called for each apply object in the order passed to Apply(...). After that, objects that implement ApplyFinalizer receive AfterApplyOro(...) in the same order.
Use ApplyFinalizer when multiple small apply objects should merge state first and perform one final action. The translation extension uses this pattern so Locale(...), Fallback(...), Write(...), and WhereTrans(...) can be combined without each object doing duplicate work.
Apply context
type ApplyContext struct {
Context context.Context
DB *oro.DB
Schema *oro.ModelSchema
Spec *oro.QuerySpec
Values oro.Map
Model any
Mode oro.ApplyMode
Stage oro.ApplyStage
State oro.Map
Rows int64
}
| Field | Meaning |
|---|---|
Context |
caller context, including deadline and extension context values |
DB |
current database session, including resolved connection for after-write apply |
Schema |
current model schema |
Spec |
mutable query specification when the stage works on a query |
Values |
write values for update/delete/restore, or insert row values where available |
Model |
current model or model slice, depending on the operation |
Mode |
operation being processed |
Stage |
lifecycle stage |
State |
shared map for all apply objects on the same query execution |
Rows |
affected row count for ApplyAfterWrite |
State is per query execution. It is the right place to merge options from several apply objects.
Modes and stages
| Mode | Stage | Trigger |
|---|---|---|
ApplyRead |
ApplyStageSpec |
before model read SQL is built |
ApplyInsert |
ApplyStageSpec |
before model insert routing/spec setup finishes |
ApplyInsert |
ApplyStageValues |
before insert values are built |
ApplyUpdate |
ApplyStageSpec |
before update SQL is built |
ApplyUpdate |
ApplyStageValues |
before update values are written |
ApplyDelete |
ApplyStageSpec |
before delete or soft-delete SQL is built |
ApplyDelete |
ApplyStageValues |
before delete or soft-delete values are written |
ApplyRestore |
ApplyStageSpec |
before restore SQL is built |
ApplyRestore |
ApplyStageValues |
before restore values are written |
ApplyAfterFind |
ApplyStageResult |
after a model is loaded |
ApplyAfterWrite |
ApplyStageResult |
after a create, upsert, or batch create succeeds |
Use ctx.IsQueryMode() when an apply only wants to modify read/update/delete/restore query conditions.
func (apply ActiveOnly) ApplyOro(ctx *oro.ApplyContext) error {
if ctx == nil || !ctx.IsQueryMode() {
return nil
}
return ctx.Where("Status", "active")
}
Context helpers
ApplyContext provides helpers that preserve Oro’s model-field rules.
| Method | Purpose |
|---|---|
Where(field, args...) |
append an AND condition |
OrWhere(field, args...) |
append an OR condition |
Select(items...) |
append select expressions |
SelectHidden(fields...) |
expose hidden model fields for internal processing |
OrderBy(fields...) |
append ascending order |
OrderByDesc(fields...) |
append descending order |
FirstRowColumns(columns...) |
read the first row using model field names |
FirstWhereColumns(field, value, columns...) |
read one row by a field condition |
RowsColumns(limit, columns...) |
read a small supporting row set |
CountRows() |
count rows for the current query spec |
Prefer these helpers over editing ctx.Spec directly. They keep model field names, table prefixes, SQL conversion, and condition validation consistent with the normal query builder.
Query apply example
This apply filters a model query by owner and keeps the normal chain available.
package owner
import "github.com/duxweb/oro"
type Apply struct {
ownerID uint64
}
func Of(ownerID uint64) Apply {
return Apply{ownerID: ownerID}
}
func (apply Apply) ApplyOro(ctx *oro.ApplyContext) error {
if ctx == nil || !ctx.IsQueryMode() {
return nil
}
return ctx.Where("OwnerID", apply.ownerID)
}
Usage:
items, err := db.Use[Document]().
Apply(owner.Of(currentUserID)).
Where("Status", "published").
Get(ctx)
Write apply example
This apply injects an actor id into create and update models before values are built.
type ActorApply struct {
id uint64
}
func Actor(id uint64) ActorApply {
return ActorApply{id: id}
}
func (apply ActorApply) ApplyOro(ctx *oro.ApplyContext) error {
if ctx == nil || ctx.Stage != oro.ApplyStageValues || ctx.Model == nil {
return nil
}
switch ctx.Mode {
case oro.ApplyInsert:
if model, ok := ctx.Model.(*Post); ok {
model.CreatedBy = oro.NullOf(apply.id)
model.UpdatedBy = oro.NullOf(apply.id)
}
case oro.ApplyUpdate:
if ctx.Values != nil {
ctx.Values["UpdatedBy"] = apply.id
}
}
return nil
}
For generic extension packages, avoid hard-coding one model type. Prefer ctx.Values for update-style writes, or define reusable embedded fields and inspect ctx.Schema.
After-write apply example
Use ApplyAfterWrite for work that should happen only after a write succeeds. logroll.Roll(...) uses this mode to clean old log rows after successful inserts.
type RollApply struct{}
func (RollApply) ApplyOro(ctx *oro.ApplyContext) error {
if ctx == nil || ctx.Mode != oro.ApplyAfterWrite || ctx.Stage != oro.ApplyStageResult {
return nil
}
if ctx.Rows == 0 {
return nil
}
// Run cleanup using ctx.DB, ctx.Schema, and ctx.Context.
return nil
}
Keep after-write work bounded. For high-frequency write paths, add throttling in the apply object or run cleanup through a scheduled job.
State merging pattern
Use ApplyFinalizer when the user should be able to compose several option objects.
const stateKey = "example"
type Apply struct {
locale string
fields []string
}
func Locale(locale string) Apply {
return Apply{locale: locale}
}
func Fields(fields ...string) Apply {
return Apply{fields: fields}
}
type state struct {
locale string
fields []string
done map[oro.ApplyStage]bool
}
func (apply Apply) ApplyOro(ctx *oro.ApplyContext) error {
s, _ := ctx.State[stateKey].(*state)
if s == nil {
s = &state{done: map[oro.ApplyStage]bool{}}
ctx.State[stateKey] = s
}
if apply.locale != "" {
s.locale = apply.locale
}
s.fields = append(s.fields, apply.fields...)
return nil
}
func (apply Apply) AfterApplyOro(ctx *oro.ApplyContext) error {
s, _ := ctx.State[stateKey].(*state)
if s == nil || s.done[ctx.Stage] {
return nil
}
s.done[ctx.Stage] = true
if ctx.Mode == oro.ApplyRead && ctx.Stage == oro.ApplyStageSpec {
for _, field := range s.fields {
if err := ctx.Select(field); err != nil {
return err
}
}
}
return nil
}
This avoids repeated mutations when several apply objects from the same extension are combined.
Boundaries
Applycurrently belongs todb.Use[T]()model queries.TableandRawqueries do not accept apply objects.Applyis query-local. It does not install global behavior.Applyshould return Oro errors or wrapped errors instead of panicking.Applyshould not store request-specific values in package globals.Applyshould use Go field names on model queries. Oro converts them to database columns during finalization.