-
-
Notifications
You must be signed in to change notification settings - Fork 239
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Question] Does Bun support optimistic locking? #1128
Comments
What is optimistic locking? UPDATE users
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 3; If I understand correctly, we dont cureently support it. |
Yes, you understand correctly.
Thank your for reply, i got it.
I think the following interface changes would be necessary to support optimistic locking:
An alternative approach could be providing a plugin that utilizes Model Query Hooks. Poc (Maybe this works only in limited usecase)package optimisticlocking
import (
"context"
"fmt"
"github.com/uptrace/bun"
)
// OptimisticLockingModel is a model that supports optimistic locking.
//
// How to implement OptimisticLockingModel is as follows:
//
// type User struct {
// ID int64 `bun:",pk,autoincrement"`
// Name string
// Version int
// }
//
// var _ optimisticlocking.OptimisticLockingModel = (*User)(nil)
//
// func (u *User) VersionColumn() string {
// return "version"
// }
//
// func (u *User) GetVersion() int {
// return u.Version
// }
//
// func (u *User) IncrementVersion() {
// u.Version++
// }
//
// func (u *User) BeforeAppendModel(ctx context.Context, query bun.Query) error {
// return optimisticlocking.BeforeAppendModel(ctx, query)
// }
//
// func (u *User) BeforeUpdate(ctx context.Context, query *bun.UpdateQuery) error {
// return optimisticlocking.BeforeUpdate(ctx, query)
// }
//
// func (u *User) BeforeDelete(ctx context.Context, query *bun.DeleteQuery) error {
// return optimisticlocking.BeforeDelete(ctx, query)
// }
//
// func (u *User) AfterUpdate(ctx context.Context, query *bun.UpdateQuery) error {
// return optimisticlocking.AfterUpdate(ctx, query)
// }
//
// func (u *User) AfterDelete(ctx context.Context, query *bun.DeleteQuery) error {
// return optimisticlocking.AfterDelete(ctx, query)
// }
type OptimisticLockingModel interface {
VersionColumn() string
GetVersion() int
IncrementVersion()
bun.BeforeAppendModelHook
bun.BeforeUpdateHook
bun.BeforeDeleteHook
bun.AfterUpdateHook
bun.AfterDeleteHook
}
// BeforeAppendModel is a hook that is called before the model is appended to the query.
// It is used to set the version column to the current version.
func BeforeAppendModel(ctx context.Context, query bun.Query) error {
model := getOptimisticLockingModelFromQuery(query)
if model == nil {
return nil
}
switch query.(type) {
case *bun.InsertQuery:
if model.GetVersion() == 0 {
model.IncrementVersion()
}
case *bun.UpdateQuery:
model.IncrementVersion()
}
return nil
}
// BeforeUpdate is a hook that is called before the model is updated.
// It is used to check if the version column is out of date.
func BeforeUpdate(ctx context.Context, query *bun.UpdateQuery) error {
model := getOptimisticLockingModelFromQuery(query)
if model == nil {
return nil
}
query.Where("? = ?", query.FQN(model.VersionColumn()), model.GetVersion())
return nil
}
// BeforeDelete is a hook that is called before the model is deleted.
// It is used to check if the version column is out of date.
func BeforeDelete(ctx context.Context, query *bun.DeleteQuery) error {
model := getOptimisticLockingModelFromQuery(query)
if model == nil {
return nil
}
query.Where("? = ?", bun.Ident(model.VersionColumn()), model.GetVersion())
return nil
}
// AfterUpdate is a hook that is called after the model is updated.
// It is used to check if the version column is out of date.
func AfterUpdate(ctx context.Context, query *bun.UpdateQuery) error {
model := getOptimisticLockingModelFromQuery(query)
if model == nil {
return nil
}
// Since cannot access the update result in AfterUpdateHook,
// detect version conflicts by checking if a record with the updated version exists.
exists, err := bun.NewSelectQuery(query.DB()).
Model(model).
ColumnExpr("1").
WherePK().
Where("? = ?", query.FQN(model.VersionColumn()), model.GetVersion()).
Exists(ctx)
if err != nil {
return err
}
if !exists {
return &OptimisticLockingError{
TableName: query.GetTableName(),
OldVersion: model.GetVersion() - 1,
}
}
return nil
}
func AfterDelete(ctx context.Context, query *bun.DeleteQuery) error {
model := getOptimisticLockingModelFromQuery(query)
if model == nil {
return nil
}
// Since cannot access the delete result in AfterDeleteHook,
// detect version conflicts by checking if a record with the updated version exists.
exists, err := bun.NewSelectQuery(query.DB()).
Model(model).
ColumnExpr("1").
WherePK().
Where("? = ?", model.VersionColumn(), model.GetVersion()).
Exists(ctx)
if err != nil {
return err
}
if !exists {
return &OptimisticLockingError{
TableName: query.GetTableName(),
OldVersion: model.GetVersion(),
}
}
return nil
}
func getOptimisticLockingModelFromQuery(query bun.Query) OptimisticLockingModel {
model := query.GetModel()
optimisticLockingModel, ok := model.Value().(OptimisticLockingModel)
if !ok {
return nil
}
return optimisticLockingModel
}
type OptimisticLockingError struct {
TableName string
OldVersion int
}
func (e *OptimisticLockingError) Error() string {
return fmt.Sprintf("bun: version %d is out of version on table %s", e.OldVersion, e.TableName)
} usage: Do you have any thoughts on these approaches? I’m open to discussing potential improvements or alternative designs. |
Here are some examples of ORMs that implement optimistic locking:
Gorm provides optimistic locking as a plugin. It offers a custom type for the version column, which is used for optimistic locking. The column type is an integer. Gorp allows specifying the version column by calling SetVersionCol("COLUMN_NAME"). The column type used for versioning is an integer. In ActiveRecord, optimistic locking is automatically enabled if the table contains a lock_version column. The column name used for optimistic locking can be customized. The column type is an integer. |
dbdriver uses duckdb, dialect with pgdialect, and fix some field type errors, my app runs ok |
Here is an example of how I do optimistic locking in my application, may not be the best apporach ,but it works for my use case. func (cr *commodityRepository) Update(ctx context.Context, com *commodity.Commodity) (*commodity.Commodity, error) {
dba, err := cr.db.DB(ctx)
if err != nil {
return nil, eris.Wrap(err, "get database connection")
}
log := cr.l.With().
Str("operation", "Update").
Str("id", com.GetID()).
Int64("version", com.Version).
Logger()
err = dba.RunInTx(ctx, nil, func(c context.Context, tx bun.Tx) error {
ov := com.Version
com.Version++
results, rErr := tx.NewUpdate().
Model(com).
WherePK().
Where("com.version = ?", ov).
Returning("*").
Exec(c)
if rErr != nil {
log.Error().
Err(rErr).
Interface("commodity", com).
Msg("failed to update commodity")
return eris.Wrap(rErr, "update commodity")
}
rows, roErr := results.RowsAffected()
if roErr != nil {
log.Error().
Err(roErr).
Interface("commodity", com).
Msg("failed to get rows affected")
return eris.Wrap(roErr, "get rows affected")
}
if rows == 0 {
return errors.NewValidationError(
"version",
errors.ErrVersionMismatch,
fmt.Sprintf("Version mismatch. The Commodity (%s) has either been updated or deleted since the last request.", com.GetID()),
)
}
return nil
})
if err != nil {
log.Error().Err(err).Msg("failed to update commodity")
return nil, eris.Wrap(err, "update commodity")
}
return com, nil
} |
Hi, maintainers!
I’m considering replacement into Bun in my project and want to check if it supports optimistic locking.
If not, is there any plan to support it in the future?
If there is no plan to support it, I’m thinking of implementing it using model query hooks(BeforeInsert, BeforeUpdate and BeforeDelete), but I’d love to hear if there are better approaches.
Thanks!
The text was updated successfully, but these errors were encountered: