Facebook/entのschema定義
前回Gormの恨み節とFacebook/entを使用した感想書いた流れで詳細書き始めたらダラダラ長くなってしまったので分割しました。
Facebook/entの基本的な使い方は公式のチュートリアルが親切なのでそちらを参照するのが良いですが、公式チュートリアルはAutoMigrationを前提としているため実際に出来上がるEntityの説明がちょい薄めなのでそこを踏まえてFacebook/entのgenerateの基となるschema定義について逆引き形式で記載していきます。
Facebook/entのschemaは主に
- DBのEntityとgolangのstructのMappingを定義する
Fields()
- Entity同士の関連(join)を定義する
Edges()
の2つで成り立っています。
EntityのPKはデフォルトでは id:int
です。
EntityのPKの名称がidでない場合は使用することは出来ますが結構苦労します。
AutoMigrationを行う場合、Entity同士のjoinのためのfieldは<edge.Toのstruct name>_<edge.Toで指定したedge name>
という名前でedge.From
のfieldに自動的に追加されます。
Facebook/entのEdgeの考え方は論理と物理schemaが入り混じってちょっとややこしいくなっているのですが
-
one-to-many
relationの場合のFK fieldはEdge.To(many)に定義し、Edge.From(one)に作成される -
one-to-one
relationの場合FKはEdge.Toに定義し、Edge.Toに作成される
となっています。
その為、実schema的にはEdge.From側にFKのfieldが定義されていても、Facebook/ent的にはEdge.To側で定義します。
具体的には User
, Car
という2つのschemaでone-to-many
relationを定義する場合
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age"),
field.String("name"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
user_cars
という名前のfieldが Car
EntityにFKとして追加されます。
このようにFacebook/entではEntityを結合するFKはFields()
には記載せずにEdges()
に記載する必要があります。
one-to-many
ralationを設定する
one-to-many
ralationを設定する場合はEdge.To
側だけの記載で問題ないです。
ただし、Edge.To
だけしか記載しない場合はEdge.From
側のidを指定してEdge.To
のレコードを取得するといったような事はできません。取得したい場合は one-to-many
ralationのmany側でone側の情報を取得するを参照してEdge.From
を設定してください。
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age"),
field.String("name"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
one-to-one
ralationを設定する
one-to-one
relationを設定する場合はedge.To
にUnique()
を指定すればよいのですが、one-to-many
の場合とは異なり、edge.To
側にrelation用のfieldとFKが作成されます。
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("group", Group.Type).
Unique(),
}
}
// Fields of the Group.
func (Group) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
}
}
many-to-many
ralationを設定する
many-to-many
の場合はEdge.To
とEdge.From
双方を設定します。
many-to-many
relationを設定した場合は紐付け用のテーブルが自動で作成されます。
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("groups", Group.Type),
}
}
// 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.From("users", User.Type).
Ref("groups"),
}
}
FKの名前を変更したい
デフォルトで名付けられる <struct name>_<edge_name>
の形式ではなく xxx_id
のように自由に名前を変更したい場合はEdge.To側でStorageKey()
を指定することで変更が行なえます。
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age"),
field.String("name"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type).
StorageKey(edge.Column("user_id")),
}
}
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
PKにid
以外を指定したい
Facebook/entはdefaultではPKはid: int
で定義されている前提でgenerateされます。idの名前を変更したいというのはよくありそうなユースケースですが何故か公式ドキュメントには載っていません。
そもそもPKをidという名前以外で定義されることが想定外…?
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("id").
StorageKey("user_id").
StructTag(`json:"user_id"`),
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
StructTagは別になくても動きますが指定してあげないとjson.Marshal()
した時に定義した名前ではなく元のid
という名称でjsonが出力されます。
{
"id": 3,
"age": 30,
"name": "a8m",
"edges": {
"Cars": null
}
}
StructTagを指定すると定義した名前でjsonが出力されます。
{
"user_id": 3,
"age": 30,
"name": "a8m",
"edges": {
"Cars": null
}
}
PKの型を変えたい
PKにUUIDやxidを使用したい場合などもあると思います。その場合はPKの型を変更する必要があります。
こちらに関しては公式ドキュメントにも載っています。
PKをstringに変更する場合
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("id").
SchemaType(map[string]string{
dialect.MySQL: "VARCHAR(20)", // Override MySQL.
dialect.Postgres: "VARCHAR(20)", // Override Postgres.
}),
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
PKをUUIDに変更する場合
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", uuid.UUID{}).
Default(func() uuid.UUID { return uuid.Must(uuid.NewRandom())}),
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
PKをxidに変更する場合
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.UUID("id", xid.ID{}).
SchemaType(map[string]string{
dialect.MySQL: "CHAR(20)", // Override MySQL.
dialect.Postgres: "CHAR(20)", // Override Postgres.
}).
Default(xid.New),
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
PKをFKに設定する
出来ません。
PKにそのEntity独自のIDではなく別のEntityのIDを指定したい場合、edge.To
にStorageKey
を指定してid
を指定すればいけそうですが実際にはidが重複してテーブル作成時にDuplicate column name error
になります。
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("meta", UserMeta.Type).
StorageKey(edge.Column("id")).
Unique(),
}
}
// Fields of the UserMeta.
func (UserMeta) Fields() []ent.Field {
return []ent.Field{
field.Int("login_count").
Positive(),
}
}
// Edges of the UserMeta.
func (UserMeta) Edges() []ent.Edge {
return []ent.Edge{
edge.From("user", User.Type).
Ref("meta").
Unique().
Required(),
}
}
-- CREATE TABLE IF NOT EXISTS `users`(`id` char(36) binary NOT NULL, `age` bigint NOT NULL, `name` varchar(255) NOT NULL DEFAULT 'unknown', `id` bigint NULL, PRIMARY KEY(`id`, `id`)) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
create table "users": Error 1060: Duplicate column name 'id'
テーブル名と異なるschemaを設定する
既にあるテーブルとstructをそのままにFacebook/entを導入したい場合、テーブル名とschema名が必ずしも一致していない事があると思います。その場合はAnnotationsをschemaに追加します。
// Annotations of the User.
func (User) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Annotation{Table: "HogeUser"},
}
}
Edge.To
側でEdge.From
側の情報を取得する - Eager Loading
one-to-many
ralationの例で説明しますがone-to-one
ralationやmany-to-many
ralationでも同じです。
Edge.To
側でEdge.From
側の情報を取得するだけの場合はEdge.From
側の設定は不要です。
取得時にWithXXX()
を設定するだけで紐づくレコードを自動で取得します。
取得したレコードはEdges
というfieldの中に格納されています。
詳細は公式ドキュメントのEager Loadingを参照してください。
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type),
}
}
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
func main() {
ctx := context.Background()
client, err := ent.Open("mysql", "mysql_dsn")
if err != nil {
log.Fatalf("failed opening connection to mysql: %v", err)
}
defer client.Close()
usr, err := client.User.Query().
Where(user.ID(1)).
WithCars(). // これを指定する
Only(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(usr.String())
for _, c := range usr.Edges.Cars {
fmt.Println(c.String())
}
}
Edge.From
側でEdge.To
側の情報を取得する
one-to-many
ralationの例で説明しますがone-to-one
ralationやmany-to-many
ralationでも同じです。
Edge.From
側のcar
Entityを取得した際にEdge.To
側のuser
Entityの情報も取得したい場合は、user
側に設定するEdge.To
と合わせてcar
側にEdge.From
の設定をする必要があります。
詳細は公式ドキュメントのEager Loadingを参照してください。
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.Int("age").
Positive(),
field.String("name").
Default("unknown"),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("cars", Car.Type), // edge.Toとedge.FromにUnique()の設定をすると one-to-one になります
}
}
// Fields of the Car.
func (Car) Fields() []ent.Field {
return []ent.Field{
field.String("model"),
field.Time("registered_at"),
}
}
// Edges of the Car.
func (Car) Edges() []ent.Edge {
return []ent.Edge{
edge.From("user", User.Type). // edge.Fromを設定しないとWithUser()を指定できない
Ref("cars"). // edge.Toのnameと一致させる
Unique(), // edge.FromにUnique()の設定がないと many-to-many になります
}
}
func main() {
ctx := context.Background()
client, err := ent.Open("mysql", "mysql_dsn")
if err != nil {
log.Fatalf("failed opening connection to mysql: %v", err)
}
defer client.Close()
c, err := client.Car.Query().
Where(car.ID(1)).
WithUser(). // これを指定する
Only(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Println(c.String())
if c.Edges.User != nil {
fmt.Println(c.Edges.User.String())
}
}