1. 程式人生 > >ent orm筆記2---schema使用(上)

ent orm筆記2---schema使用(上)

在上一篇關於快速使用ent orm的筆記中,我們再最開始使用`entc init User` 建立schema,在ent orm 中的schema 其實就是資料庫模型,在schema中我們可以通過Fields 定義資料庫中表的欄位資訊;通過Edges 定義表之間的關係資訊;通過Index 定義欄位的索引資訊等等,這篇文章會整理一下關於ent orm 中如何使用這些。 備註:文章中的所有程式碼在`github.com/peanut-cc/ent_orm_notes` ## Fileds 當我們執行 `entc init User` 之後,會在當前目錄下生成一個ent目錄,在該目錄下有一個schema目錄,預設情況下schema/user.go檔案如下: ```go package schema import "github.com/facebook/ent" // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return nil } // Edges of the User. func (User) Edges() []ent.Edge { return nil } ``` 如果要對user 這表新增欄位,需要在Fileds方法中新增如下所示: ```go // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.Int("age"), field.String("username"). Unique(), field.Time("created_at"). Default(time.Now), field.Float32("salary"). Optional(), } } ``` **注意: 預設情況下,所有欄位都是必填欄位,可以使用Optional方法將其設定為optional。** ### 資料型別 下面的資料型別都是支援的: * All Go numeric types. Like int, uint8, float64, etc. * bool * string * time.Time * []byte (only supported by SQL dialects). * JSON (only supported by SQL dialects). * Enum (only supported by SQL dialects). ```go // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.Int("age"), field.String("username"). Unique(), field.Time("created_at"). Default(time.Now), field.Float32("salary"). Optional(), field.Bool("active"). Default(false), field.JSON("strings", []string{}). Optional(), field.Enum("state"). Values("on", "off"). Optional(), } } ``` ### ID欄位 資料庫表的id欄位,預設是內建的,不需要單獨新增,其型別預設為int, 並在資料庫中自動遞增, 為了將id配置為在所有表中唯一,需要在schema migration的時候使用WithGlobalUniqueID 如果需要對id欄位進行其他配置,或者想要使用UUID格式存id,則需要覆蓋id的配置。 ```go // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.Int("id"). StructTag(`json:"oid,omitempty"`), } } ``` ```go // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.UUID("id", uuid.UUID{}), } } ``` ```go // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("id"). MaxLen(25). NotEmpty(). Unique(). Immutable(), } } ``` ### 資料庫型別 每個資料庫都有自己的從go的資料型別到資料庫型別的對映,例如,Mysql 在資料庫中將float64欄位建立為雙精度的。ent orm 有一個選項引數可以使用SchemaType 方法覆蓋預設行為 ```go // Fields of the Card. func (Card) Fields() []ent.Field { return []ent.Field{ field.Float("amount"). SchemaType(map[string]string{ dialect.MySQL: "decimal(6,2)", // Override MySQL. dialect.Postgres: "numeric", // Override Postgres. }), } } ``` ### GO Type 欄位的預設型別是基本的Go資料型別,例如,對於字串欄位,型別為string, 對於時間欄位,型別為time.Time GoType 方法提供了一個選項,可以使用自定義型別替換預設的ent型別。但自定義型別必須是可以轉換為Go的基本型別的型別,或者實現了[ValueScanner](https://pkg.go.dev/github.com/facebook/ent/schema/field?tab=doc#ValueScanner)介面的型別 ```go // Fields of the Card. func (Card) Fields() []ent.Field { return []ent.Field{ field.Float("amount"). GoType(Amount(0)), field.String("name"). Optional(). // A ValueScanner type. GoType(&sql.NullString{}), } } ``` ### Default Values 預設值 Non-unique 的欄位可以通過Default 和 UpdateDefault方法設定預設值 ```go // Fields of the Group. func (Group) Fields() []ent.Field { return []ent.Field{ field.Time("created_at"). Default(time.Now), field.Time("updated_at"). Default(time.Now). UpdateDefault(time.Now), } } ``` ### Validators 關於欄位的validator是通過 func(T) error 函式,該函式使用Validate方法在schema中定義,並在建立或更新schema的時候應用於欄位的校驗 欄位的validator支援的型別有string 和所有的數字型別 ```go // Fields of the Group. func (Group) Fields() []ent.Field { return []ent.Field{ field.String("name"). Match(regexp.MustCompile("[a-zA-Z_]+$")). Validate(func(s string) error { if strings.ToLower(s) == s { return errors.New("group name must begin with uppercase") } return nil }), } } ``` ### 內建的 Validators ent orm 提供了一些內建的validators, 如下: * Numeric types: Positive() - Positive adds a minimum value validator with the value of 1 Negative() - Negative adds a maximum value validator with the value of -1 NonNegative() - NonNegative adds a minimum value validator with the value of 0 Min(i) - Validate that the given value is > i. Max(i) - Validate that the given value is < i. Range(i, j) - Validate that the given value is within the range [i, j]. * string: MinLen(i) MaxLen(i) Match(regexp.Regexp) ### Optional 可選欄位 可選欄位是在建立的時候不是必傳的欄位,並將在資料庫設定為可為空的欄位 預設情況下,欄位都是必填欄位 ### Nillable 有時候你可能希望區分欄位的零值和nil,如資料庫的某列包含0 或者NULL,Nillable選項正是為此而存在的. 如果有一個型別為T的欄位設定為Nillable ,在通過go generate 生成程式碼的時候的時候生成的struct 中改欄位的型別是*T, 如果資料庫中該欄位是NULL, 那麼在ent orm的查詢結果中就是nil, 否則對於沒有設定Nillable的欄位,如果資料庫中欄位的值是NULl,返回的則是改欄位的零值 ```go // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("required_name"), field.String("optional_name").Optional(), field.String("nilable_name").Optional().Nillable(), field.String("nilable_name2").Optional().Nillable(), field.Int("age").Optional(), field.Int("age2").Optional().Nillable(), } } ``` 我們通過如下程式碼進行資料的建立和查詢,這裡分別建立了兩條資料,第一條資料的設定SetOptionalName,SetNilableName 的欄位都沒有設定內容,第二次的時候都設定了內容 ```go package main import ( "context" "log" "github.com/peanut-cc/ent_orm_notes/schema_notes/ent/user" _ "github.com/go-sql-driver/mysql" "github.com/peanut-cc/ent_orm_notes/schema_notes/ent" ) func main() { client, err := ent.Open("mysql", "root:123456@tcp(10.211.55.3:3306)/schema_notes?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) } client.User.Create().SetRequiredName("peanut").Save(ctx) client.User.Create().SetRequiredName("syncd"). SetOptionalName("option_name"). SetNilableName("nil_name"). SetNilableName2("nil_name2"). SetAge(18). SetAge2(20). SaveX(ctx) u := client.User.Query().Where(user.RequiredNameEQ("peanut")).OnlyX(ctx) log.Printf("required_name is:%v option_name is:%v nil_name is:%v nil_name2 is:%v age is :%v age2 is:%v\n", u.RequiredName, u.OptionalName, u.NilableName, u.NilableName2, u.Age, u.Age2) u2 := client.User.Query().Where(user.RequiredNameEQ("syncd")).OnlyX(ctx) log.Printf("required_name is:%v option_name is:%v nil_name is:%v nil_name2 is:%v age is :%v age2 is:%v\n", u2.RequiredName, u2.OptionalName, u2.NilableName, u2.NilableName2, u2.Age, u2.Age2) } ``` 下面是資料的列印結果: ```shell 2020/08/26 20:39:47 required_name is:peanut option_name is: nil_name is: nil_name2 is: age is :0 age2 is: 2020/08/26 20:39:47 required_name is:syncd option_name is:option_name nil_name is:0xc000200580 nil_name2 is:0xc000200590 age is :18 age2 is:0xc00020c4d8 ``` ### Immutable 不可變欄位 不可變欄位,是隻能在建立的時候設定值 ```go // Fields of the user. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), field.Time("created_at"). Default(time.Now). Immutable(), } } ``` ### Uniqueness 唯一索引 可以使用Unique方法給欄位設定唯一索引。 注意:唯一所以欄位不能具有預設值 ```go // Fields of the user. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), field.String("nickname"). Unique(), } } ``` ### Storage Key 可以使用StorageKey方法配置自定義儲存名稱。在SQL中對映為列名 ```go // Fields of the user. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"). StorageKey(`old_name"`), } } ``` ### Indexes 索引 可以在多個欄位和一些關係表中建立索引 ### Struct Tags 可以使用StructTag方法將自定義struct tag新增到生成的實體中。 請注意,如果未提供此選項,或者提供的該選項不包含json標記,則預設json標記將與欄位名稱一起新增。 ```go // Fields of the user. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"). StructTag(`gqlgen:"gql_name"`), } } ``` ### Sensitive Fields 可以使用Sensitive方法將字串欄位定義為Sensitive Fields。 Sensitive Fields不會被列印,並且在編碼時將被忽略。 請注意,Sensitive Fields不能具有struct標記。 ```go // Fields of the user. func (User) Fields() []ent.Field { return []ent.Field{ field.String("password"). Sensitive(), } } ``` ### Annotations 在程式碼生成中,Annotations用於將任意元資料附加到欄位物件。模板擴充套件可以檢索這個元資料並在它們的模板中使用它。注意,元資料物件必須可序列化為 JSON 原始值(例如,struct、 map 或 slice)。 ```go // Fields of the user. func (User) Fields() []ent.Field { return []ent.Field{ field.Time("creation_date"). Annotations(entgql.Annotation{ OrderField: "CREATED_AT", }), } } ``` ## Edges ### 快速使用 Edges 也理解為表之間的association,通常指的我們表之間的一對多,多對多關係等。 ![er-group-users](https://entgo.io/assets/er_user_pets_groups.png) `ent/schema/pet.go` ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // Pet holds the schema definition for the Pet entity. type Pet struct { ent.Schema } // Fields of the Pet. func (Pet) Fields() []ent.Field { return []ent.Field{ field.String("name"), } } // Edges of the Pet. func (Pet) Edges() []ent.Edge { return []ent.Edge{ edge.From("owner", User.Type). Ref("pets"). Unique(), } } ``` `ent/schema/user.go` ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), field.Int("age"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("pets", Pet.Type), edge.From("groups", Group.Type).Ref("users"), } } ``` `ent/schema/group.go` ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // Group holds the schema definition for the Group entity. type Group struct { ent.Schema } // Fields of the Group. func (Group) Fields() []ent.Field { return []ent.Field{ field.String("name"), } } // Edges of the Group. func (Group) Edges() []ent.Edge { return []ent.Edge{ edge.To("users", User.Type), } } ``` 在上面的關係中,一個使用者可以有多個寵物,但是一個寵物只能屬於一個使用者。所以這裡對於寵物來說是一對一的關係,對於使用者來說是多對一關係。 我們檢視一下建立的pets表的資訊: ```sql CREATE TABLE `pets` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) COLLATE utf8mb4_bin NOT NULL, `user_pets` bigint(20) DEFAULT NULL, PRIMARY KEY (`id`), KEY `pets_users_pets` (`user_pets`), CONSTRAINT `pets_users_pets` FOREIGN KEY (`user_pets`) REFERENCES `users` (`id`) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ``` 因為在上述關係中:使用者和寵物之間是一對多關係,所以這裡使用的`edge.To`; 而對寵物來說是一對一的關係,所以這裡使用`edge.From`的`Ref` `edge.To`和`edge.From` 是建立表關係的兩個方法 ### 一對一關係 ![er-user-card](https://entgo.io/assets/er_user_card.png) 在這個例子中,設定一個使用者只能有一張信用卡,而一個信用卡也只能屬於一個使用者。 `ent/schema/user.go` ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), field.Int("age"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("card", Card.Type).Unique(), } } ``` `ent/schema/card.go` ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // Card holds the schema definition for the Card entity. type Card struct { ent.Schema } // Fields of the Card. func (Card) Fields() []ent.Field { return []ent.Field{ field.String("number"), field.Time("expired"), } } // Edges of the Card. func (Card) Edges() []ent.Edge { return []ent.Edge{ edge.From("owner", User.Type). Ref("card"). Unique(). // We add the "Required" method to the builder // to make this edge required on entity creation. // i.e. Card cannot be created without its owner. Required(), } } ``` ### 一對多關係 ![er-user-pets](https://entgo.io/assets/er_user_pets.png) 在這個例子中使用者和寵物之間是一對多關係,每個使用者可以有多個寵物,一個寵物只有一個主人 `ent/schema/user.go` ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("pets", Pet.Type), } } ``` `ent/schema/pet.go` ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // Pet holds the schema definition for the Pet entity. type Pet struct { ent.Schema } // Fields of the Pet. func (Pet) Fields() []ent.Field { return []ent.Field{ field.String("name"), } } // Edges of the Pet. func (Pet) Edges() []ent.Edge { return []ent.Edge{ edge.From("owner", User.Type). Ref("pets"). Unique(), } } ``` 相關的查詢如下程式碼: ```go package main import ( "context" "fmt" "log" _ "github.com/go-sql-driver/mysql" "github.com/peanut-cc/ent_orm_notes/one_to_many/ent" ) func main() { client, err := ent.Open("mysql", "root:123456@tcp(192.168.1.100:3306)/one_to_one?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) error { // Create the 2 pets. pedro, err := client.Pet. Create(). SetName("pedro"). Save(ctx) if err != nil { return fmt.Errorf("creating pet: %v", err) } lola, err := client.Pet. Create(). SetName("lola"). Save(ctx) if err != nil { return fmt.Errorf("creating pet: %v", err) } // Create the user, and add its pets on the creation. // 建立使用者,並新增使用者和寵物的關係 a8m, err := client.User. Create(). SetName("a8m"). AddPets(pedro, lola). Save(ctx) if err != nil { return fmt.Errorf("creating user: %v", err) } fmt.Println("User created:", a8m)shell // Output: User(id=1, age=30, name=a8m) // Query the owner. Unlike `Only`, `OnlyX` panics if an error occurs. // 根據寵物反向查詢所屬的使用者 owner := pedro.QueryOwner().OnlyX(ctx) fmt.Println(owner.Name) // Output: a8m // Traverse the sub-graph. Unlike `Count`, `CountX` panics if an error occurs. // 根據寵物反向查詢使用者,並查詢該使用者有多少寵物 count := pedro. QueryOwner(). // a8m QueryPets(). // pedro, lola CountX(ctx) // count fmt.Println(count) // Output: 2 return nil } ``` ### 多對多關係 ![er-user-groups](https://entgo.io/assets/er_user_groups.png) 在這個例子中,使用者和組之間是多對多關係,每個組有多個使用者,每個使用者也可以加入多個組 `ent/schema/group.go` ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // Group holds the schema definition for the Group entity. type Group struct { ent.Schema } // Fields of the Group. func (Group) Fields() []ent.Field { return []ent.Field{ field.String("name"), } } // Edges of the Group. func (Group) Edges() []ent.Edge { return []ent.Edge{ edge.To("users", User.Type), } } ``` ent/schema/user.go ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.From("groups", Group.Type).Ref("users"), } } ``` 這個時候,會生成第三張表,group_users表,查看錶的資訊如下: ```sql CREATE TABLE `group_users` ( `group_id` bigint(20) NOT NULL, `user_id` bigint(20) NOT NULL, PRIMARY KEY (`group_id`,`user_id`), KEY `group_users_user_id` (`user_id`), CONSTRAINT `group_users_group_id` FOREIGN KEY (`group_id`) REFERENCES `groups` (`id`) ON DELETE CASCADE, CONSTRAINT `group_users_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ``` 常用的查詢方法: ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.From("groups", Group.Type).Ref("users"), } } ``` ### 多對多(單張表) ![er-following-followers](https://entgo.io/assets/er_following_followers.png) 這種關係其實也挺常見的,如我們微博賬戶,不同賬戶之間可以相關關注 ```go package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.package schema import ( "github.com/facebook/ent" "github.com/facebook/ent/schema/edge" "github.com/facebook/ent/schema/field" ) // User holds the schema definition for the User entity. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.String("name"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("following", User.Type). From("followers"), } } Field { return []ent.Field{ field.String("name"), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("following", User.Type). From("followers"), } } ``` 這樣會生成一個user_following表,表資訊為: ```sql CREATE TABLE `user_following` ( `user_id` bigint(20) NOT NULL, `follower_id` bigint(20) NOT NULL, PRIMARY KEY (`user_id`,`follower_id`), KEY `user_following_follower_id` (`follower_id`), CONSTRAINT `user_following_follower_id` FOREIGN KEY (`follower_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, CONSTRAINT `user_following_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ``` ### 其他 在這裡和在Fields中同樣也有`Required` ` StorageKey` `Indexes` `Annotations` 用法基本一樣,這裡不再說明 ## 延伸閱讀 - https://e