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.

Bulk upsert returns affected rows:

affected, err := db.Table("products").UpsertMany(ctx, []oro.Map{
    {"code": "P001", "price": 120},
    {"code": "P002", "price": 180},
}, oro.ConflictBy("code").UpdateAll())

For bulk upsert, Update / UpdateAll use each row’s own inserted values. Use UpdateMap when the conflict update should be a shared constant or expression for the whole statement. Table queries have no schema magic, so use UpdateAllExcept("created_at") when a column should be inserted but not overwritten on conflicts.

First or create

product, created, err := db.Use[Product]().FirstOrCreate(ctx, &Product{
    Code:  "P001",
    Price: 120,
}, "Code")

FirstOrCreate is the get-or-create helper for unique keys. It performs an initial read, inserts with DO NOTHING only when missing, then reads the row back, so concurrent callers return the same row instead of creating duplicates. Use Go field names for models and database columns for table queries.

The conflict fields or columns must correspond to a unique constraint. A soft-deleted placeholder that still owns the unique key returns ErrScan because the visible read cannot see it and the insert is skipped. Existing query conditions are preserved for both lookup reads, so extra filters can also make the read-back miss the inserted row.

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