mirror of
https://github.com/ent/ent.git
synced 2026-04-28 05:30:56 +03:00
examples: update version example to follow the locking blog post
This commit is contained in:
committed by
Ariel Mashraki
parent
cdfa3e35bb
commit
9704c4c87b
@@ -1,9 +1,10 @@
|
||||
# [Optimistic Lock](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) Using Hooks
|
||||
# [Optimistic Lock](https://en.wikipedia.org/wiki/Optimistic_concurrency_control)
|
||||
|
||||
In this example, we implement an optimistic locking mechanism using an `ent.Hook`.
|
||||
In this example, we implement an optimistic locking mechanism using the technique mentioned in
|
||||
[Ent Blog](https://entgo.io/blog/2021/07/22/database-locking-techniques-with-ent/).
|
||||
|
||||
The idea is to add to our schema a `version` field that holds the Unix time of when the latest update occurred.
|
||||
When an `UpdateOne` operation is executed, the hook updates the `version` field with the new value and adds a predicate
|
||||
When an `Update` operation is executed, the hook updates the `version` field with the new value and adds a predicate
|
||||
to verify that the `version` wasn't updated by another process/transaction during the mutation.
|
||||
|
||||
An error is returned if the versions are mismatched, and the user should reload the entity and retry the mutation.
|
||||
|
||||
@@ -212,6 +212,5 @@ func (c *UserClient) GetX(ctx context.Context, id int) *User {
|
||||
|
||||
// Hooks returns the client hooks.
|
||||
func (c *UserClient) Hooks() []Hook {
|
||||
hooks := c.hooks.User
|
||||
return append(hooks[:len(hooks):len(hooks)], user.Hooks[:]...)
|
||||
return c.hooks.User
|
||||
}
|
||||
|
||||
@@ -9,4 +9,4 @@
|
||||
// Package internal holds a loadable version of the latest schema.
|
||||
package internal
|
||||
|
||||
const Schema = `{"Schema":"entgo.io/ent/examples/version/ent/schema","Package":"entgo.io/ent/examples/version/ent","Schemas":[{"name":"User","config":{"Table":""},"fields":[{"name":"version","type":{"Type":13,"Ident":"","PkgPath":"","Nillable":false,"RType":null},"default":true,"default_kind":19,"position":{"Index":0,"MixedIn":true,"MixinIndex":0},"comment":"Unix time of when the latest update occurred"},{"name":"status","type":{"Type":6,"Ident":"user.Status","PkgPath":"","Nillable":false,"RType":null},"enums":[{"N":"online","V":"online"},{"N":"offline","V":"offline"}],"position":{"Index":0,"MixedIn":false,"MixinIndex":0}}],"hooks":[{"Index":0,"MixedIn":true,"MixinIndex":0},{"Index":1,"MixedIn":true,"MixinIndex":0}]}],"Features":["schema/snapshot"]}`
|
||||
const Schema = `{"Schema":"entgo.io/ent/examples/version/ent/schema","Package":"entgo.io/ent/examples/version/ent","Schemas":[{"name":"User","config":{"Table":""},"fields":[{"name":"version","type":{"Type":13,"Ident":"","PkgPath":"","Nillable":false,"RType":null},"default":true,"default_kind":19,"position":{"Index":0,"MixedIn":true,"MixinIndex":0},"comment":"Unix time of when the latest update occurred"},{"name":"status","type":{"Type":6,"Ident":"user.Status","PkgPath":"","Nillable":false,"RType":null},"enums":[{"N":"online","V":"online"},{"N":"offline","V":"offline"}],"position":{"Index":0,"MixedIn":false,"MixinIndex":0}}]}],"Features":["schema/snapshot"]}`
|
||||
|
||||
@@ -6,4 +6,22 @@
|
||||
|
||||
package ent
|
||||
|
||||
// The schema-stitching logic is generated in entgo.io/ent/examples/version/ent/runtime/runtime.go
|
||||
import (
|
||||
"entgo.io/ent/examples/version/ent/schema"
|
||||
"entgo.io/ent/examples/version/ent/user"
|
||||
)
|
||||
|
||||
// The init function reads all schema descriptors with runtime code
|
||||
// (default values, validators, hooks and policies) and stitches it
|
||||
// to their package variables.
|
||||
func init() {
|
||||
userMixin := schema.User{}.Mixin()
|
||||
userMixinFields0 := userMixin[0].Fields()
|
||||
_ = userMixinFields0
|
||||
userFields := schema.User{}.Fields()
|
||||
_ = userFields
|
||||
// userDescVersion is the schema descriptor for version field.
|
||||
userDescVersion := userMixinFields0[0].Descriptor()
|
||||
// user.DefaultVersion holds the default value on creation for the version field.
|
||||
user.DefaultVersion = userDescVersion.Default.(func() int64)
|
||||
}
|
||||
|
||||
@@ -6,28 +6,7 @@
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"entgo.io/ent/examples/version/ent/schema"
|
||||
"entgo.io/ent/examples/version/ent/user"
|
||||
)
|
||||
|
||||
// The init function reads all schema descriptors with runtime code
|
||||
// (default values, validators, hooks and policies) and stitches it
|
||||
// to their package variables.
|
||||
func init() {
|
||||
userMixin := schema.User{}.Mixin()
|
||||
userMixinHooks0 := userMixin[0].Hooks()
|
||||
user.Hooks[0] = userMixinHooks0[0]
|
||||
user.Hooks[1] = userMixinHooks0[1]
|
||||
userMixinFields0 := userMixin[0].Fields()
|
||||
_ = userMixinFields0
|
||||
userFields := schema.User{}.Fields()
|
||||
_ = userFields
|
||||
// userDescVersion is the schema descriptor for version field.
|
||||
userDescVersion := userMixinFields0[0].Descriptor()
|
||||
// user.DefaultVersion holds the default value on creation for the version field.
|
||||
user.DefaultVersion = userDescVersion.Default.(func() int64)
|
||||
}
|
||||
// The schema-stitching logic is generated in entgo.io/ent/examples/version/ent/runtime.go
|
||||
|
||||
const (
|
||||
Version = "(devel)" // Version of ent codegen.
|
||||
|
||||
@@ -5,17 +5,9 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
gen "entgo.io/ent/examples/version/ent"
|
||||
"entgo.io/ent/examples/version/ent/hook"
|
||||
"entgo.io/ent/examples/version/ent/predicate"
|
||||
"entgo.io/ent/examples/version/ent/user"
|
||||
|
||||
"entgo.io/ent"
|
||||
"entgo.io/ent/dialect/sql"
|
||||
"entgo.io/ent/schema/field"
|
||||
"entgo.io/ent/schema/mixin"
|
||||
)
|
||||
@@ -56,52 +48,3 @@ func (VersionMixin) Fields() []ent.Field {
|
||||
Comment("Unix time of when the latest update occurred"),
|
||||
}
|
||||
}
|
||||
|
||||
// Hooks of the VersionMixin.
|
||||
func (VersionMixin) Hooks() []ent.Hook {
|
||||
return []ent.Hook{
|
||||
// Apply the `OptimisticLock` hook only on `UpdateOne` operations,
|
||||
// and block all `Update` (update-many) operations as we don't have
|
||||
// access to the nodes that are affected by these mutation.
|
||||
hook.On(OptimisticLock(), ent.OpUpdateOne),
|
||||
hook.Reject(ent.OpUpdate),
|
||||
}
|
||||
}
|
||||
|
||||
func OptimisticLock() ent.Hook {
|
||||
return func(next ent.Mutator) ent.Mutator {
|
||||
return hook.UserFunc(func(ctx context.Context, m *gen.UserMutation) (ent.Value, error) {
|
||||
oldV, err := m.OldVersion(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
curV, exists := m.Version()
|
||||
if !exists {
|
||||
curV = time.Now().UnixNano()
|
||||
m.SetVersion(curV)
|
||||
} else if curV <= oldV {
|
||||
return nil, fmt.Errorf("version field must be > previous value: %v <= %v", curV, oldV)
|
||||
}
|
||||
// Add an SQL predicate that validates the "version" column is equal
|
||||
// to "oldV" (it wasn't changed during the mutation by another process).
|
||||
m.Where(MatchVersion(oldV, curV))
|
||||
v, err := next.Mutate(ctx, m)
|
||||
if gen.IsNotFound(err) {
|
||||
id, _ := m.ID()
|
||||
return nil, fmt.Errorf("user %d was changed by another process", id)
|
||||
}
|
||||
return v, err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// MatchVersion returns a "dynamic User predicate". First, it checks that the "version"
|
||||
// field was not changed by another process when executing the `UPDATE` operation. Next,
|
||||
// it checks that the new value matches when `SELECT`-ing the node from the database.
|
||||
func MatchVersion(oldV, curV int64) predicate.User {
|
||||
p := user.Version(oldV)
|
||||
return func(s *sql.Selector) {
|
||||
p(s)
|
||||
p = user.Version(curV)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,6 @@ package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"entgo.io/ent"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,14 +40,7 @@ func ValidColumn(column string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Note that the variables below are initialized by the runtime
|
||||
// package on the initialization of the application. Therefore,
|
||||
// it should be imported in the main as follows:
|
||||
//
|
||||
// import _ "entgo.io/ent/examples/version/ent/runtime"
|
||||
//
|
||||
var (
|
||||
Hooks [2]ent.Hook
|
||||
// DefaultVersion holds the default value on creation for the "version" field.
|
||||
DefaultVersion func() int64
|
||||
)
|
||||
|
||||
@@ -54,9 +54,7 @@ func (uc *UserCreate) Save(ctx context.Context) (*User, error) {
|
||||
err error
|
||||
node *User
|
||||
)
|
||||
if err := uc.defaults(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uc.defaults()
|
||||
if len(uc.hooks) == 0 {
|
||||
if err = uc.check(); err != nil {
|
||||
return nil, err
|
||||
@@ -115,15 +113,11 @@ func (uc *UserCreate) ExecX(ctx context.Context) {
|
||||
}
|
||||
|
||||
// defaults sets the default values of the builder before save.
|
||||
func (uc *UserCreate) defaults() error {
|
||||
func (uc *UserCreate) defaults() {
|
||||
if _, ok := uc.mutation.Version(); !ok {
|
||||
if user.DefaultVersion == nil {
|
||||
return fmt.Errorf("ent: uninitialized user.DefaultVersion (forgotten import ent/runtime?)")
|
||||
}
|
||||
v := user.DefaultVersion()
|
||||
uc.mutation.SetVersion(v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// check runs all checks and user-defined validators on the builder.
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"entgo.io/ent/examples/version/ent"
|
||||
_ "entgo.io/ent/examples/version/ent/runtime"
|
||||
@@ -32,15 +33,23 @@ func Example_OptimisticLock() {
|
||||
fmt.Println(usr.ID, usr.Status)
|
||||
|
||||
usrCopy := client.User.Query().OnlyX(ctx)
|
||||
usrCopy = usrCopy.Update().SetStatus(user.StatusOffline).SaveX(ctx)
|
||||
fmt.Println(usrCopy.ID, usrCopy.Status)
|
||||
affected := client.User.Update().
|
||||
Where(user.ID(usrCopy.ID), user.Version(usrCopy.Version)).
|
||||
SetStatus(user.StatusOffline).
|
||||
SetVersion(time.Now().UnixNano()).
|
||||
SaveX(ctx)
|
||||
fmt.Println(affected)
|
||||
|
||||
// The operation fails because the user was updated by another process (usrCopy).
|
||||
_, err = usr.Update().SetStatus(user.StatusOffline).Save(ctx)
|
||||
fmt.Println(err)
|
||||
// The operation won't updated the database because the user was updated by another process (usrCopy).
|
||||
affected = client.User.Update().
|
||||
Where(user.ID(usr.ID), user.Version(usr.Version)).
|
||||
SetStatus(user.StatusOffline).
|
||||
SetVersion(time.Now().UnixNano()).
|
||||
SaveX(ctx)
|
||||
fmt.Println(affected)
|
||||
|
||||
// Output:
|
||||
// 1 online
|
||||
// 1 offline
|
||||
// user 1 was changed by another process
|
||||
// 1
|
||||
// 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user