Hooks
Hooks允许你在对表做一些操作的时候添加一些自定义的处理逻辑。如当我对某个表的某条数据进行更新的前和后,做一些自定义的操作,其实这个就和web 中 middleware 中间件的原理类似
具体我们可以一下的操作中增加hook:
Create
- Create node in the graph.
UpdateOne
- Update a node in the graph. For example, increment its field.
Update
- Update multiple nodes in the graph that match a predicate.
DeleteOne
- Delete a node from the graph.
Delete
- Delete all nodes that match a predicate.
mutation hooks 有两种类型:schema hooks 和 runtime hooks
schema hooks 主要是在schema 中添加一些自定义操作
runtime hooks 主要是用在像logging, metrics, tracing,等等
Runtime hooks
这里通过一个简单的例子理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
package main
import (
"context"
"log"
"time"
_ "github.com/go-sql-driver/mysql"
"github.com/peanut-cc/ent_orm_notes/hook_runtime_example/ent"
)
func main() {
client, err := ent.Open("mysql", "root:123456@tcp(10.211.55.3:3306)/hook_runtime?parseTime=True")
if err != nil {
log.Fatalf("ent open db error:%v", err)
}
defer client.Close()
ctx := context.Background()
// run the auto migration tool
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources:%v", err)
}
client.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
start := time.Now()
defer func() {
log.Printf("Op=%s\tType=%s\tTime=%s\tConreteType=%T", m.Op(), m.Type(), time.Since(start), m)
}()
return next.Mutate(ctx, m)
})
})
Do(ctx, client)
}
func Do(ctx context.Context, client *ent.Client) {
client.User.Create().SetName("peanut").SaveX(ctx)
}
|
日志打印如下:
2020/09/02 11:44:55 Op=OpCreate Type=User Time=4.535547ms ConreteType=*ent.UserMutation
这里是添加了一个全局的hook, 记录了我们对数据库每条记录操作的耗时,这个在日常的工作中也是非常有用的。
但是在某些场景下,我们可能有更细粒度的使用,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
func main() {
// <client was defined in the previous block>
// Add a hook only on user mutations.
client.User.Use(func(next ent.Mutator) ent.Mutator {
// Use the "<project>/ent/hook" to get the concrete type of the mutation.
return hook.UserFunc(func(ctx context.Context, m *ent.UserMutation) (ent.Value, error) {
return next.Mutate(ctx, m)
})
})
// Add a hook only on update operations.
client.Use(hook.On(Logger(), ent.OpUpdate|ent.OpUpdateOne))
// Reject delete operations.
client.Use(hook.Reject(ent.OpDelete|ent.OpDeleteOne))
}
|
如果希望共享一个在多种类型(例如 Group 和 User)之间变化字段的钩子。有两种方法可以做到这一点:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// Option 1: use type assertion.
client.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if ns, ok := m.(interface{ SetName(string) }); ok {
ns.SetName("Ariel Mashraki")
}
return next.Mutate(ctx, m)
})
})
// Option 2: use the generic ent.Mutation interface.
client.Use(func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if err := m.SetField("name", "Ariel Mashraki"); err != nil {
// An error is returned, if the field is not defined in
// the schema, or if the type mismatch the field type.
}
return next.Mutate(ctx, m)
})
})
|
Schema hooks
schema 在定义schema的时候使用,并且仅应用于schema type的更改,这个初衷是把schem 中关于节点类型相关的所有逻辑集中在一个地方,代码例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
package schema
import (
"context"
"fmt"
gen "<project>/ent"
"<project>/ent/hook"
"github.com/facebook/ent"
)
// Card holds the schema definition for the CreditCard entity.
type Card struct {
ent.Schema
}
// Hooks of the Card.
func (Card) Hooks() []ent.Hook {
return []ent.Hook{
// First hook.
hook.On(
func(next ent.Mutator) ent.Mutator {
return hook.CardFunc(func(ctx context.Context, m *gen.CardMutation) (ent.Value, error) {
if num, ok := m.Number(); ok && len(num) < 10 {
return nil, fmt.Errorf("card number is too short")
}
return next.Mutate(ctx, m)
})
},
// Limit the hook only for these operations.
ent.OpCreate|ent.OpUpdate|ent.OpUpdateOne,
),
// Second hook.
func(next ent.Mutator) ent.Mutator {
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
if s, ok := m.(interface{ SetName(string) }); ok {
s.SetName("Boring")
}
return next.Mutate(ctx, m)
})
},
}
}
|
请注意,如果使用schema hook,则必须在main包中添加以下导入:
import _ "<project>/ent/runtime"
Evaluation order
Hook按照它们注册到client的顺序被调用。因此client.Use(f,g,h)在mutations执行 f (g (h (…))。
还要注意,runtime hooks在schema hooks.之前调用。也就是说,如果 g 和 h 是在schema中定义的,而 f 是使用 client.Use(…)注册的。 它们将被执行如下: f (g (h (…)))。
Transactions
开始一个事务,例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
// GenTx generates group of entities in a transaction.
func GenTx(ctx context.Context, client *ent.Client) error {
tx, err := client.Tx(ctx)
if err != nil {
return fmt.Errorf("starting a transaction: %v", err)
}
hub, err := tx.Group.
Create().
SetName("Github").
Save(ctx)
if err != nil {
return rollback(tx, fmt.Errorf("failed creating the group: %v", err))
}
// Create the admin of the group.
dan, err := tx.User.
Create().
SetAge(29).
SetName("Dan").
AddManage(hub).
Save(ctx)
if err != nil {
return rollback(tx, err)
}
// Create user "Ariel".
a8m, err := tx.User.
Create().
SetAge(30).
SetName("Ariel").
AddGroups(hub).
AddFriends(dan).
Save(ctx)
if err != nil {
return rollback(tx, err)
}
fmt.Println(a8m)
// Output:
// User(id=2, age=30, name=Ariel)
// Commit the transaction.
return tx.Commit()
}
// rollback calls to tx.Rollback and wraps the given error
// with the rollback error if occurred.
func rollback(tx *ent.Tx, err error) error {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%v: %v", err, rerr)
}
return err
}
|
有些情况,如果你现有的代码已经可以用*ent.Client
正常执行,这个时候,你希望这个执行过程添加事务的功能,这种情况下,可以通过如下方式实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// WrapGen wraps the existing "Gen" function in a transaction.
func WrapGen(ctx context.Context, client *ent.Client) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
txClient := tx.Client()
// Use the "Gen" below, but give it the transactional client; no code changes to "Gen".
if err := Gen(ctx, txClient); err != nil {
return rollback(tx, err)
}
return tx.Commit()
}
// Gen generates a group of entities.
func Gen(ctx context.Context, client *ent.Client) error {
// ...
return nil
}
|
最佳实践
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
func WithTx(ctx context.Context, client *ent.Client, fn func(tx *ent.Tx) error) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
defer func() {
if v := recover(); v != nil {
tx.Rollback()
panic(v)
}
}()
if err := fn(tx); err != nil {
if rerr := tx.Rollback(); rerr != nil {
err = errors.Wrapf(err, "rolling back transaction: %v", rerr)
}
return err
}
if err := tx.Commit(); err != nil {
return errors.Wrapf(err, "committing transaction: %v", err)
}
return nil
}
func Do(ctx context.Context, client *ent.Client) {
// WithTx helper.
if err := WithTx(ctx, client, func(tx *ent.Tx) error {
return Gen(ctx, tx.Client())
}); err != nil {
log.Fatal(err)
}
}
|
像schema hooks 和runtime hooks 一样,hooks可以注册在事务上,并且将会被执行在Tx.Commitor
Tx.Rollback
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
func Do(ctx context.Context, client *ent.Client) error {
tx, err := client.Tx(ctx)
if err != nil {
return err
}
// Add a hook on Tx.Commit.
tx.OnCommit(func(next ent.Committer) ent.Committer {
return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error {
// Code before the actual commit.
err := next.Commit(ctx, tx)
// Code after the transaction was committed.
return err
})
})
// Add a hook on Tx.Rollback.
tx.OnRollback(func(next ent.Rollbacker) ent.Rollbacker {
return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error {
// Code before the actual rollback.
err := next.Rollback(ctx, tx)
// Code after the transaction was rolled back.
return err
})
})
//
// <Code goes here>
//
return err
}
|
Migration
ent 提供了数据库迁移支持,可以使用数据库表结构与ent/migrate/schema
中定义的对象保持一致。
Auto Migration
在程序的初始化过程中,运行auto-migration
1
2
3
|
if err := client.Schema.Create(ctx); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
|
Create 将创建 ent 项目所需的所有数据库资源。默认情况下,Create 在“ append-only”模式下工作; 这意味着,它只创建新的表和索引,将列追加到表或扩展列类型。例如,将 int 改为 bigint。
删除列或索引怎么样?
Drop Resources
WithDropIndex 和 WithDropColumn 两个选项用于删除表列和索引。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
err = client.Schema.Create(
ctx,
migrate.WithDropIndex(true),
migrate.WithDropColumn(true),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
|
为了在调试模式下运行迁移(打印所有 SQL 查询) ,请运行:
1
2
3
4
5
6
7
8
|
err := client.Debug().Schema.Create(
ctx,
migrate.WithDropIndex(true),
migrate.WithDropColumn(true),
)
if err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
|
Universal IDs
默认情况下,每个表的 SQL 主键从1开始; 这意味着不同类型的多个实体可以共享相同的 ID。不像 AWS Neptune, 是 uuid。
如果使用 GraphQL,这将不能很好地工作,因为它要求对象 ID 是唯一的。
要启用项目的 Universal-IDs 支持,请将 WithGlobalUniqueID 选项传递给迁移。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"context"
"log"
"<project>/ent"
"<project>/ent/migrate"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Run migration.
if err := client.Schema.Create(ctx, migrate.WithGlobalUniqueID(true)); err != nil {
log.Fatalf("failed creating schema resources: %v", err)
}
}
|
它是如何工作的?Ent 迁移为每个实体(表)的 id 分配1 « 32范围,并将此信息存储在名为 ent _ types 的表中。例如,a 类型的 id 的范围是[1,4294967296] ,b 类型的 id 范围是[4294967296,8589934592]等。
请注意,如果启用此选项,则可能的表的最大数量为65535。
Offline Mode
脱机模式允许您将模式更改写入 io。然后在数据库中执行它们。这对于在数据库上执行 SQL 命令之前验证它们或手动运行 SQL 脚本非常有用。
Print changes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
package main
import (
"context"
"log"
"os"
"<project>/ent"
"<project>/ent/migrate"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Dump migration changes to stdout.
if err := client.Schema.WriteTo(ctx, os.Stdout); err != nil {
log.Fatalf("failed printing schema changes: %v", err)
}
}
|
Write changes to a file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import (
"context"
"log"
"os"
"<project>/ent"
"<project>/ent/migrate"
)
func main() {
client, err := ent.Open("mysql", "root:pass@tcp(localhost:3306)/test")
if err != nil {
log.Fatalf("failed connecting to mysql: %v", err)
}
defer client.Close()
ctx := context.Background()
// Dump migration changes to an SQL script.
f, err := os.Create("migrate.sql")
if err != nil {
log.Fatalf("create migrate file: %v", err)
}
defer f.Close()
if err := client.Schema.WriteTo(ctx, f); err != nil {
log.Fatalf("failed printing schema changes: %v", err)
}
}
|
sql.DB Integration
下面的示例演示如何将自定义 sql.db 对象传递给 ent.Client。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package main
import (
"time"
"<your_project>/ent"
"github.com/facebook/ent/dialect/sql"
)
func Open() (*ent.Client, error) {
drv, err := sql.Open("mysql", "<mysql-dsn>")
if err != nil {
return nil, err
}
// Get the underlying sql.DB object of the driver.
db := drv.DB()
db.SetMaxIdleConns(10)
db.SetMaxOpenConns(100)
db.SetConnMaxLifetime(time.Hour)
return ent.NewClient(ent.Driver(drv)), nil
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package main
import (
"database/sql"
"time"
"<your_project>/ent"
entsql "github.com/facebook/ent/dialect/sql"
)
func Open() (*ent.Client, error) {
db, err := sql.Open("mysql", "<mysql-dsn>")
if err != nil {
return nil, err
}
db.SetMaxIdleConns(10)
db.SetMaxOpenConns(100)
db.SetConnMaxLifetime(time.Hour)
// Create an ent.Driver from `db`.
drv := entsql.OpenDB("mysql", db)
return ent.NewClient(ent.Driver(drv)), nil
}
|
Use Opencensus With MySQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
package main
import (
"context"
"database/sql"
"database/sql/driver"
"<project>/ent"
"contrib.go.opencensus.io/integrations/ocsql"
"github.com/go-sql-driver/mysql"
entsql "github.com/facebook/ent/dialect/sql"
)
type connector struct {
dsn string
}
func (c connector) Connect(context.Context) (driver.Conn, error) {
return c.Driver().Open(c.dsn)
}
func (connector) Driver() driver.Driver {
return ocsql.Wrap(
mysql.MySQLDriver{},
ocsql.WithAllTraceOptions(),
ocsql.WithRowsClose(false),
ocsql.WithRowsNext(false),
ocsql.WithDisableErrSkip(true),
)
}
// Open new connection and start stats recorder.
func Open(dsn string) *ent.Client {
db := sql.OpenDB(connector{dsn})
// Create an ent.Driver from `db`.
drv := entsql.OpenDB("mysql", db)
return ent.NewClient(ent.Driver(drv))
}
|
Use pgx with PostgreSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package main
import (
"context"
"database/sql"
"log"
"<project>/ent"
"github.com/facebook/ent/dialect"
entsql "github.com/facebook/ent/dialect/sql"
_ "github.com/jackc/pgx/v4/stdlib"
)
// Open new connection
func Open(databaseUrl string) *ent.Client {
db, err := sql.Open("pgx", databaseUrl)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Create an ent.Driver from `db`.
drv := entsql.OpenDB(dialect.Postgres, db)
return ent.NewClient(ent.Driver(drv))
}
func main() {
client := Open("postgresql://user:password@127.0.0.1/database")
// Your code. For example:
ctx := context.Background()
if err := client.Schema.Create(ctx); err != nil {
log.Fatal(err)
}
users, err := client.User.Query().All(ctx)
if err != nil {
log.Fatal(err)
}
log.Println(users)
}
|
总结
对en orm 整个文档进行整理学习之后,计划后续通过ent orm ,gin web框架实现一个blog,这样也算是对于ent orm 有一个简单的应用
延伸阅读