1. 程式人生 > >ent orm筆記4---Code Generation

ent orm筆記4---Code Generation

在前面幾篇文章中,我們經常使用的可能就是entc這個命令了,entc這個工具給帶來了很多功能,這篇文章主要整理關於ent orm 中Code Generation 之前的例子中有個知識點少整理了,就是關於如果我們想要看orm在執行過程中詳細原生sql語句是可以開啟Debug看到的,程式碼如下: ```go client, err := ent.Open("mysql", "root:123456@tcp(10.211.55.3:3306)/graph_traversal?parseTime=True",ent.Debug()) ``` ## 序言 ### Initialize A New Schema 通過類似如下命令可以生成Schema 模板: ```shell entc init User Pet ``` init 將在ent/schema 目錄下建立兩個schema user.go 和 pet.go ,如果ent目錄不存在,則會建立 ### Generate Assets 在添加了fields 和 edges 後,可以在專案的根目錄執行entc generate 或者使用go generate 生成程式碼 ```shell go generate ./ent ``` Generate 命令生成以下內容: - 用於與graph 互動的Client 和Tx物件 - schema 的CRUD生成器 - 每個schema型別的Entity物件 - 用於與構建互動的常量和斷言 - SQL方言的migrate 包 ### Version Compatibility Between `entc` And `ent` 這裡主要是關於在專案中使用ent 的時候ent的版本要和entc的包的版本相同,並且專案中使用Go modules 進行包管理 ### Code Generation Options 要了解更多關於 codegen 選項的資訊,entc generate -h : ```shell generate go code for the schema directory Usage: entc generate [flags] path Examples: entc generate ./ent/schema entc generate github.com/a8m/x Flags: --header string override codegen header -h, --help help for generate --idtype [int int64 uint uint64 string] type of the id field (default int) --storage string storage driver to support in codegen (default "sql") --target string target directory for codegen --template strings external templates to execute ``` ### Storage entc 可以為 SQL 和 Gremlin 方言生成資產。 ### External Templates 接受要執行的外部 Go 模板。如果模板名稱已經由 entc 定義,它將覆蓋現有的名稱。否則,它將把執行輸出寫入與模板同名的檔案。Flag 格式支援如下檔案、目錄和 glob: ```shell entc generate --template --template glob="path/to/*.tmpl" ./ent/schema ``` 更多的資訊和例子可以在外部模板文件中找到 [external templates doc]: https://entgo.io/docs/templates/ ### Use `entc` As A Package 執行 entc 的另一個選項是將其作為一個包使用,如下所示: ```go package main import ( "log" "github.com/facebook/ent/entc" "github.com/facebook/ent/entc/gen" "github.com/facebook/ent/schema/field" ) func main() { err := entc.Generate("./schema", &gen.Config{ Header: "// Your Custom Header", IDType: &field.TypeInfo{Type: field.TypeInt}, }) if err != nil { log.Fatal("running ent codegen:", err) } } ``` ### Schema Description 如果想要得到我們定義的schema的描述資訊,可以通過如下命令: ```shell entc describe ./ent/schema ``` 以之前的例子中執行效果如下: ````shell User: +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+ | Field | Type | Unique | Optional | Nillable | Default | UpdateDefault | Immutable | StructTag | Validators | +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+ | id | | false | false | false | false | false | false | json:"id,omitempty" | 0 | | name | string | false | false | false | false | false | false | json:"name,omitempty" | 0 | +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+ +-----------+------+---------+-----------+----------+--------+----------+ | Edge | Type | Inverse | BackRef | Relation | Unique | Optional | +-----------+------+---------+-----------+----------+--------+----------+ | followers | User | true | following | M2M | false | true | | following | User | false | | M2M | false | true | +-----------+------+---------+-----------+----------+--------+----------+ ```` ## CRUD API ### Create A New Client **MySQL** ```go package main import ( "log" "/ent" _ "github.com/go-sql-driver/mysql" ) func main() { client, err := ent.Open("mysql", ":@tcp(:)/?parseTime=True") if err != nil { log.Fatal(err) } defer client.Close() } ``` **PostgreSQL** ```go package main import ( "log" "/ent" _ "github.com/lib/pq" ) func main() { client, err := ent.Open("postgres","host= port= user= dbname= password=") if err != nil { log.Fatal(err) } defer client.Close() } ``` **SQLite** ```go package main import ( "log" "/ent" _ "github.com/mattn/go-sqlite3" ) func main() { client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") if err != nil { log.Fatal(err) } defer client.Close() } ``` **Gremlin (AWS Neptune)** ```go package main import ( "log" "/ent" ) func main() { client, err := ent.Open("gremlin", "http://localhost:8182") if err != nil { log.Fatal(err) } } ``` ### Create An Entity **Save** a user. ```go a8m, err := client.User. // UserClient. Create(). // User create builder. SetName("a8m"). // Set field value. SetNillableAge(age). // Avoid nil checks. AddGroups(g1, g2). // Add many edges. SetSpouse(nati). // Set unique edge. Save(ctx) // Create and return. ``` **SaveX** a pet; Unlike **Save**, **SaveX** panics if an error occurs. ```go pedro := client.Pet. // PetClient. Create(). // Pet create builder. SetName("pedro"). // Set field value. SetOwner(a8m). // Set owner (unique edge). SaveX(ctx) // Create and return. ``` ### Create Many **Save** a bulk of pets ```go names := []string{"pedro", "xabi", "layla"} bulk := make([]*ent.PetCreate, len(names)) for i, name := range names { bulk[i] = client.Pet.Create().SetName(name).SetOwner(a8m) } pets, err := client.Pet.CreateBulk(bulk...).Save(ctx) ``` ### Update One 更新一個從資料庫返回的entity ```go a8m, err = a8m.Update(). // User update builder. RemoveGroup(g2). // Remove specific edge. ClearCard(). // Clear unique edge. SetAge(30). // Set field value Save(ctx) // Save and return. ``` ### Update By ID ```go pedro, err := client.Pet. // PetClient. UpdateOneID(id). // Pet update builder. SetName("pedro"). // Set field name. SetOwnerID(owner). // Set unique edge, using id. Save(ctx) // Save and return. ``` ### Update Many 以斷言進行過濾 ```go n, err := client.User. // UserClient. Update(). // Pet update builder. Where( // user.Or( // (age >= 30 OR name = "bar") user.AgeEQ(30), // user.Name("bar"), // AND ), // user.HasFollowers(), // UserHasFollowers() ). // SetName("foo"). // Set field name. Save(ctx) // exec and return. ``` 通過edge 斷言進行查詢 ```go n, err := client.User. // UserClient. Update(). // Pet update builder. Where( // user.HasFriendsWith( // UserHasFriendsWith ( user.Or( // age = 20 user.Age(20), // OR user.Age(30), // age = 30 ) // ) ), // ). // SetName("a8m"). // Set field name. Save(ctx) // exec and return. ``` ### Query The Graph 獲取所有使用者的關注者 ```go users, err := client.User. // UserClient. Query(). // User query builder. Where(user.HasFollowers()). // filter only users with followers. All(ctx) // query and return. ``` 獲取特定使用者的所有跟隨者; 從graph中的一個節點開始遍歷 ```go users, err := a8m. QueryFollowers(). All(ctx) ``` 獲取所有寵物的名字 ```go names, err := client.Pet. Query(). Select(pet.FieldName). Strings(ctx) ``` 獲取所有寵物的名字和年齡 ```go var v []struct { Age int `json:"age"` Name string `json:"name"` } err := client.Pet. Query(). Select(pet.FieldAge, pet.FieldName). Scan(ctx, &v) if err != nil { log.Fatal(err) } ``` ### Delete One 這個用於如果我們已經通過client查詢到了一個entity,然後想要刪除這條記錄: ```go err := client.User. DeleteOne(a8m). Exec(ctx) ``` Delete by ID. ```go err := client.User. DeleteOneID(id). Exec(ctx) ``` ### Delete Many 使用斷言進行刪除 ```go err := client.File. Delete(). Where(file.UpdatedAtLT(date)) Exec(ctx) ``` ### Mutation 通過 entc init 生成的每個schema 都有自己的mutaion,例如我們通過 entc init User Pet, 在通過go generate ./ent 生成的程式碼中有 `ent/mutation.go` 在該檔案中定義了: ```go ..... // UserMutation represents an operation that mutate the Users // nodes in the graph. type UserMutation struct { config op Op typ string id *int name *string age *int addage *int clearedFields map[string]struct{} done bool oldValue func(context.Context) (*User, error) } ..... // PetMutation represents an operation that mutate the Pets // nodes in the graph. type PetMutation struct { config op Op typ string id *int name *string age *int addage *int clearedFields map[string]struct{} done bool oldValue func(context.Context) (*Pet, error) } ``` 例如,所有的[`User` builders](https://entgo.io/docs/crud#create-an-entity)都共享相同的UserMutaion 物件,左右的builder 型別都繼承通用的[`ent.Mutation`](https://pkg.go.dev/github.com/facebook/ent?tab=doc#Mutation)介面. 這裡所說的 user builders,拿User schema來說指的是`UserCreate`、`UserDelete`、`UserQuery`、`UserUpdate` 物件,go generate 生成的程式碼中,我們可以到 `./ent/user_create.go、./ent/user_delete.go、./ent/user_query.go、./ent/user_update.go`檔案中看到如下定義: ```go // ./ent/user_create.go // UserCreate is the builder for creating a User entity. type UserCreate struct { config mutation *UserMutation hooks []Hook } //./ent/user_delete.go // UserDelete is the builder for deleting a User entity. type UserDelete struct { config hooks []Hook mutation *UserMutation predicates []predicate.User } // ./ent/user_query.go // UserQuery is the builder for querying User entities. type UserQuery struct { config limit *int offset *int order []OrderFunc unique []string predicates []predicate.User // intermediate query (i.e. traversal path). sql *sql.Selector path func(context.Context) (*sql.Selector, error) } // ./ent/user_update.go // UserUpdate is the builder for updating User entities. type UserUpdate struct { config hooks []Hook mutation *UserMutation predicates []predicate.User } ``` 在下面的例子中,ent.UserCreate 和 ent.UserUpdate 都使用一個通用的方法對age 和name 列進行操作: ```go package main import ( "context" "log" _ "github.com/go-sql-driver/mysql" "github.com/peanut-cc/ent_orm_notes/aboutMutaion/ent" ) func main() { client, err := ent.Open("mysql", "root:123456@tcp(10.211.55.3:3306)/aboutMutaion?parseTime=True") if err != nil { log.Fatal(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) } Do(ctx, client) } func Do(ctx context.Context, client *ent.Client) { creator := client.User.Create() SetAgeName(creator.Mutation()) creator.SaveX(ctx) updater := client.User.UpdateOneID(1) SetAgeName(updater.Mutation()) updater.SaveX(ctx) } // SetAgeName sets the age and the name for any mutation. func SetAgeName(m *ent.UserMutation) { m.SetAge(32) m.SetName("Ariel") } ``` 在某些情況下,你希望對多個不同的型別應用同一個方法,對於這種情況,要麼使用通用的ent.Mutation 介面,或者自己實現一個介面,程式碼如下: ```go func Do2(ctx context.Context, client *ent.Client) { creator1 := client.User.Create().SetAge(18) SetName(creator1.Mutation(), "a8m") creator1.SaveX(ctx) creator2 := client.Pet.Create().SetAge(16) SetName(creator2.Mutation(), "pedro") creator2.SaveX(ctx) } // SetNamer wraps the 2 methods for getting // and setting the "name" field in mutations. type SetNamer interface { SetName(string) Name() (string, bool) } func SetName(m SetNamer, name string) { if _, exist := m.Name(); !exist { m.SetName(name) } } ``` ## Graph Traversal 在這個部分的例子中會使用如下的Graph ![er-traversal-graph](https://entgo.io/assets/er_traversal_graph.png) ![er-traversal-graph-gopher](https://entgo.io/assets/er_traversal_graph_gopher.png) 上面的遍歷從一個 Group 實體開始,繼續到它的 admin (edge) ,繼續到它的朋友(edge) ,獲取他們的寵物(edge) ,獲取每個寵物的朋友(edge) ,並請求它們的主人 ```go func Traverse(ctx context.Context, client *ent.Client) error { owner, err := client.Group. // GroupClient. Query(). // Query builder. Where(group.Name("Github")). // Filter only Github group (only 1). QueryAdmin(). // Getting Dan. QueryFriends(). // Getting Dan's friends: [Ariel]. QueryPets(). // Their pets: [Pedro, Xabi]. QueryFriends(). // Pedro's friends: [Coco], Xabi's friends: []. QueryOwner(). // Coco's owner: Alex. Only(ctx) // Expect only one entity to return in the query. if err != nil { return fmt.Errorf("failed querying the owner: %v", err) } fmt.Println(owner) // Output: // User(id=3, age=37, name=Alex) return nil } ``` 下面的遍歷如何? ![er-traversal-graph-gopher-query](https://entgo.io/assets/er_traversal_graph_gopher_query.png) 我們希望得到所有寵物(entities)的所有者(edge)是朋友(edge)的一些群管理員(edge)。 ``` func Traverse2(ctx context.Context, client *ent.Client) error { pets, err := client.Pet. Query(). Where( pet.HasOwnerWith( user.HasFriendsWith( user.HasManage(), ), ), ). All(ctx) if err != nil { return fmt.Errorf("failed querying the pets: %v", err) } fmt.Println(pets) // Output: // [Pet(id=1, name=Pedro) Pet(id=2, name=Xabi)] return nil } ``` 上面的查詢中,查詢所有的寵物,條件是: 寵物要有主人,同時寵物的主人是要有朋友,同時該主人還要屬於管理員 ## Eager Loading ent 支援通過它們的edges 查詢,並將關聯的entities 新增到返回的物件中 通過下面的例子理解: ![er-group-users](https://entgo.io/assets/er_user_pets_groups.png) 查詢上面關係中所有使用者和它們的寵物,程式碼如下: ```go func edgerLoading(ctx context.Context, client *ent.Client) { users, err := client.User.Query().WithPets().All(ctx) if err != nil { log.Fatalf("user query failed:%v", err) } log.Println(users) for _, u := range users { for _, p := range u.Edges.Pets { log.Printf("user (%v) -- > Pet (%v)\n", u.Name, p.Name) } } } ``` 完整的程式碼在:https://github.com/peanut-cc/ent_orm_notes/graph_traversal 查詢的結果如下: ```shell 2020/09/01 20:09:07 [User(id=1, age=29, name=Dan) User(id=2, age=30, name=Ariel) User(id=3, age=37, name=Alex) User(id=4, age=18, name=peanut)] 2020/09/01 20:09:07 user (Ariel) -- > Pet (Pedro) 2020/09/01 20:09:07 user (Ariel) -- > Pet (Xabi) 2020/09/01 20:09:07 user (Alex) -- > Pet (Coco) ``` 預載入允許查詢多個關聯,包括巢狀關聯,還可以過濾,排序或限制查詢結果,例如: ```go func edgerLoading2(ctx context.Context, client *ent.Client) { users, err := client.User. Query(). Where( user.AgeGT(18), ). WithPets(). WithGroups(func(q *ent.GroupQuery) { q.Limit(5) q.WithUsers().Limit(5) }).All(ctx) if err != nil { log.Fatalf("user query failed:%v", err) } log.Println(users) for _, u := range users { for _, p := range u.Edges.Pets { log.Printf("user (%v) --> Pet (%v)\n", u.Name, p.Name) } for _, g := range u.Edges.Groups { log.Printf("user (%v) -- Group (%v)\n", u.Name, g.Name) } } } ``` 每個query-builder都有一個方法列表,其形式為 `With(...func(Query))` ``代表邊緣名稱(像`WithGroups`) ,`< N>` 代表邊緣型別(像`GroupQuery`)。 注意,只有 SQL 方言支援這個特性 ## Aggregation ### Group By 按所有使用者的姓名和年齡欄位分組,並計算其總年齡。 ```go package main import ( "context" "log" "github.com/peanut-cc/ent_orm_notes/groupBy/ent/user" _ "github.com/go-sql-driver/mysql" "github.com/peanut-cc/ent_orm_notes/groupBy/ent" ) func main() { client, err := ent.Open("mysql", "root:123456@tcp(10.211.55.3:3306)/groupBy?parseTime=True", ent.Debug()) if err != nil { log.Fatal(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) } GenData(ctx, client) Do(ctx, client) } func GenData(ctx context.Context, client *ent.Client) { client.User.Create().SetName("peanut").SetAge(18).SaveX(ctx) client.User.Create().SetName("jack").SetAge(20).SaveX(ctx) client.User.Create().SetName("steve").SetAge(22).SaveX(ctx) client.User.Create().SetName("peanut-cc").SetAge(18).SaveX(ctx) client.User.Create().SetName("jack-dd").SetAge(18).SaveX(ctx) } func Do(ctx context.Context, client *ent.Client) { var v []struct { Name string `json:"name"` Age int `json:"age"` Sum int `json:"sum"` Count int `json:"count"` } client.User. Query(). GroupBy( user.FieldName, user.FieldAge, ). Aggregate( ent.Count(), ent.Sum(user.FieldAge), ). ScanX(ctx, &v) log.Println(v) } ``` 按一個欄位分組,例子如下: ```go func Do2(ctx context.Context, client *ent.Client) { names := client.User.Query().GroupBy(user.FieldName).StringsX(ctx) log.Println(names) } ``` ## Predicates ### Field Predicates - Bool: - =, != - Numberic: - =, !=, >, <, >=, <=, - IN, NOT IN - Time: - =, !=, >, <, >=, <= - IN, NOT IN - String: - =, !=, >, <, >=, <= - IN, NOT IN - Contains, HasPrefix, HasSuffix - ContainsFold, EqualFold (**SQL** specific) - Optional fields: - IsNil, NotNil ### Edge Predicates **HasEdge** 例如,查詢所有寵物的所有者,使用: ```go client.Pet. Query(). Where(pet.HasOwner()). All(ctx) ``` **HasEdgeWith** ```go client.Pet. Query(). Where(pet.HasOwnerWith(user.Name("a8m"))). All(ctx) ``` ### Negation (NOT) ```go client.Pet. Query(). Where(pet.Not(pet.NameHasPrefix("Ari"))). All(ctx) ``` ### Disjunction (OR) ```go client.Pet. Query(). Where( pet.Or( pet.HasOwner(), pet.Not(pet.HasFriends()), ) ). All(ctx) ``` ### Conjunction (AND) ```go client.Pet. Query(). Where( pet.And( pet.HasOwner(), pet.Not(pet.HasFriends()), ) ). All(ctx) ``` ### Custom Predicates 如果想編寫自己的特定於方言的邏輯,Custom predicates可能很有用。 ```go pets := client.Pet. Query(). Where(predicate.Pet(func(s *sql.Selector) { s.Where(sql.InInts(pet.OwnerColumn, 1, 2, 3)) })). AllX(ctx) ``` ## Paging And Ordering ### Limit 將查詢結果限制為 n 個實體。 ```go users, err := client.User. Query(). Limit(n). All(ctx) ``` ### Offset 設定從查詢返回的第一個最大數量。 ```go users, err := client.User. Query(). Offset(10). All(ctx) ``` ### Ordering Order 返回按一個或多個欄位的值排序的實體。 ```go users, err := client.User.Query(). Order(ent.Asc(user.FieldName)). All(ctx) ``` ## 延伸閱讀 - https://e