OOro

Update & Delete

Conditional updates, atomic increments, soft delete, restore, force delete, optimistic locking, and safety rules.

Oro has one update entry: Update(ctx, oro.Map{...}). There is no Save, because “save this struct by primary key” and “update rows matching this condition” should not be ambiguous.

Conditional update

affected, err := db.Use[Product]().
    Where("Code", "P001").
    Update(ctx, oro.Map{"Price": 120})

Model queries use Go field names inside oro.Map. Table queries use database column names:

affected, err := db.Table("products").
    Where("code", "P001").
    Update(ctx, oro.Map{"price": 120})

The return value is the affected row count.

Why updates use Map

A Go struct cannot say whether Price: 0 means “set price to zero” or “the caller did not provide price”. Oro uses oro.Map so every updated field is explicit.

Atomic increments

_, err := db.Use[Product]().Where("ID", id).Update(ctx, oro.Map{
    "Stock": oro.Decrement(1),
    "Sold":  oro.Increment(1),
})

Table queries use column names:

_, err := db.Table("products").Where("id", id).Update(ctx, oro.Map{
    "stock": oro.Decrement(1),
})

Raw expressions

_, err := db.Table("products").Where("id", id).Update(ctx, oro.Map{
    "updated_at": oro.Raw("CURRENT_TIMESTAMP"),
})

Use raw expressions only for trusted SQL fragments.

Only and Omit

_, err := db.Use[Product]().
    Where("ID", id).
    Update(ctx, oro.Map{"Price": 120, "UpdatedAt": time.Now()}, oro.Only("Price"))
_, err := db.Use[Product]().
    Where("ID", id).
    Update(ctx, oro.Map{"Price": 120}, oro.Omit("UpdatedAt"))

Optimistic lock

func (Product) Define(s *oro.SchemaBuilder) {
    s.Field("Version").UnsignedBigInt().Default(1).OptimisticLock()
}
rows, err := db.Use[Product]().
    Where("ID", id).
    Update(ctx, oro.Map{"Price": 120}, oro.CheckVersion(version))

A version mismatch returns oro.ErrStaleData.

Upsert

saved, err := db.Use[Product]().Upsert(ctx, product,
    oro.ConflictBy("Code").Update("Price", "Stock"),
)

Table upsert:

row, err := db.Table("products").Upsert(ctx, oro.Map{
    "code":  "P001",
    "price": 120,
}, oro.ConflictBy("code").Update("price"))

ConflictBy uses Go field names for models and database column names for table queries.

Delete

affected, err := db.Use[Product]().Where("ID", id).Delete(ctx)

If the model has a soft-delete field, Delete writes the deleted timestamp. Otherwise it physically deletes the row.

Enable the conventional soft-delete field explicitly:

type Product struct {
    oro.Model
    softdelete.SoftDeleteFields // DeletedAt -> deleted_at
}

Table deletes are physical deletes:

affected, err := db.Table("products").Where("id", id).Delete(ctx)

Soft delete

product, err := db.Use[Product]().WithDeleted().Find(ctx, id)
products, err := db.Use[Product]().OnlyDeleted().Get(ctx)

Restore:

_, err := db.Use[Product]().WithDeleted().Where("ID", id).Restore(ctx)

Force physical delete:

_, err := db.Use[Product]().Where("ID", id).ForceDelete(ctx)

Safety guard

Oro rejects unconditional update and delete operations:

  • oro.ErrUnsafeUpdate
  • oro.ErrUnsafeDelete

For full-table maintenance, write the scope explicitly or use controlled raw SQL.

Edit this page