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.ErrUnsafeUpdateoro.ErrUnsafeDelete
For full-table maintenance, write the scope explicitly or use controlled raw SQL.