Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

逆引きFacebook/ent

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-manyrelationの場合のFK fieldはEdge.To(many)に定義し、Edge.From(one)に作成される
  • one-to-onerelationの場合FKはEdge.Toに定義し、Edge.Toに作成される

となっています。

その為、実schema的にはEdge.From側にFKのfieldが定義されていても、Facebook/ent的にはEdge.To側で定義します。

具体的には User, Carという2つのschemaでone-to-many relationを定義する場合

ent/schema/user.go
// 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),
    }
}
ent/schema/car.go
// 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として追加されます。

image

このように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を設定してください。

image

ent/schema/user.go
// 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),
    }
}
ent/schema/car.go
// 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.ToUnique()を指定すればよいのですが、one-to-manyの場合とは異なり、edge.To側にrelation用のfieldとFKが作成されます。

image

ent/schema/user.go
// 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(),
    }
}
ent/schema/group.go
// Fields of the Group.
func (Group) Fields() []ent.Field {
    return []ent.Field{
        field.String("name"),
    }
}

many-to-many ralationを設定する

many-to-manyの場合はEdge.ToEdge.From双方を設定します。
many-to-many relationを設定した場合は紐付け用のテーブルが自動で作成されます。

image

ent/schema/user.go
// 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),
    }
}
ent/schema/group.go
// 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()を指定することで変更が行なえます。

image

ent/schema/user.go
// 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")),
    }
}
ent/schema/car.go
// 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という名前以外で定義されることが想定外…?

image

ent/schema/user.go
// 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にUUIDxidを使用したい場合などもあると思います。その場合はPKの型を変更する必要があります。
こちらに関しては公式ドキュメントにも載っています

PKをstringに変更する場合

image

ent/schema/user.go
// 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に変更する場合

image

ent/schema/user.go
// 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に変更する場合

image

ent/schema/user.go
// 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.ToStorageKeyを指定してidを指定すればいけそうですが実際にはidが重複してテーブル作成時にDuplicate column name errorになります。

image

ent/schema/user.go
// 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(),
    }
}
ent/schema/user_meta.go
// 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に追加します。

image

ent/schema/user.go
// 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でも同じです。

image

Edge.To側でEdge.From側の情報を取得するだけの場合はEdge.From側の設定は不要です。
取得時にWithXXX()を設定するだけで紐づくレコードを自動で取得します。
取得したレコードはEdgesというfieldの中に格納されています。
詳細は公式ドキュメントのEager Loadingを参照してください。

ent/schema/user.go
// 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),
    }
}
ent/schema/car.go
// Fields of the Car.
func (Car) Fields() []ent.Field {
    return []ent.Field{
        field.String("model"),
        field.Time("registered_at"),
    }
}
main.go
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でも同じです。

image

Edge.From側のcar Entityを取得した際にEdge.To側のuser Entityの情報も取得したい場合は、user側に設定するEdge.Toと合わせてcar側にEdge.Fromの設定をする必要があります。
詳細は公式ドキュメントのEager Loadingを参照してください。

ent/schema/user.go
// 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 になります
    }
}
ent/schema/car.go
// 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 になります
    }
}
main.go
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())
    }
}
tomtwinkle
golangおいしいです。
beex-inc
BeeXではクラウドネイティブアプリ開発、企業の基幹クラウド基盤構築、システム移行、運用保守を行っています。
https://www.beex-inc.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away