GoのORM entのEdgesと生成されるTableとSQLなど

18 min read読了の目安(約16800字

GoのORM entを触る機会がありドキュメントを斜め読みして、ははーんFieldsにテーブル構成書いて、Edgesにテーブル間の関係を書けば良きにはかられってくれるのかなと勝手に思っていたのが実際に触ってみるとけっこう違ったのでentのEdgesの挙動を確認してみたmemo

entとは

https://entgo.io/

Simple, yet powerful entity framework for Go, that makes it easy to build > and maintain applications with large data-models.

  • Schema As Code - model any database schema as Go objects.
  • Easily Traverse Any Graph - run queries, aggregations and traverse any graph structure easily.
  • Statically Typed And Explicit API - 100% statically typed and explicit API using code generation.
  • Multi Storage Driver - supports MySQL, PostgreSQL, SQLite and Gremlin.
  • Extendable - simple to extend and customize using Go templates.

entはGoのORMでSchemaをcodeで定義でき、簡単にグラフ構造のデータの処理でき、静的な型付け、複数種類のDBの対応、拡張可能という特徴を持っているGoのORMになります。
この記事ではentのEdgesという機能についてになります。

entのEdgesとは

https://entgo.io/docs/schema-edges/
entのEdgesは、entityの関係(関連付け)になります。
下記のような
  • itemとitem descriptionは1:1
  • itemとitem variationは1:n
  • itemとitem categoryはn:n
    のようなものです。

entのEdgesでは上記のようなentity間の関連をcodeで定義できます。それを元にqueryなどすることができます。

Edgesを定義する

これからは下記のsample codeを元に進めていきます。

https://github.com/ogataka50/ent-test

上記のitemの例をもとにそれぞれEdgesを定義してみました。

// Fields of the Item.
func (Item) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
	}
}

// Edges of the Item.
func (Item) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("item_description", ItemDescription.Type).Unique(),
		edge.To("item_variation", ItemVariation.Type),
		edge.To("item_group", ItemGroup.Type),
	}
}
// Fields of the ItemDescription.
func (ItemDescription) Fields() []ent.Field {
	return []ent.Field{
		field.String("description"),
	}
}

// Edges of the ItemDescription.
func (ItemDescription) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("owner", Item.Type).
			Ref("item_description").
			Required().Unique(),
	}
}
// Fields of the ItemVariation.
func (ItemVariation) Fields() []ent.Field {
	return []ent.Field{
		field.String("variant_name"),
	}
}

// Edges of the ItemVariation.
func (ItemVariation) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("origin_item", Item.Type).
			Ref("item_variation").
			Required().Unique(),
	}
}
// Fields of the ItemGroup.
func (ItemGroup) Fields() []ent.Field {
	return []ent.Field{
		field.String("name"),
	}
}

// Edges of the ItemGroup.
func (ItemGroup) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("group_item", Item.Type).
			Ref("item_group"),
	}
}

上記のような定義後にent describeを実行するとそれぞれの関係を確認することができます。

$ ent describe ./ent/schema

Item:
        +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+
        | Field |  Type  | Unique | Optional | Nillable | Default | UpdateDefault | Immutable |       StructTag       | Validators |
        +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+
        | id    | int    | 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 |
        +------------------+-----------------+---------+---------+----------+--------+----------+
        | item_description | ItemDescription | false   |         | O2O      | true   | true     |
        | item_variation   | ItemVariation   | false   |         | O2M      | false  | true     |
        | item_group       | ItemGroup       | false   |         | M2M      | false  | true     |
        +------------------+-----------------+---------+---------+----------+--------+----------+

ItemDescription:
        +-------------+--------+--------+----------+----------+---------+---------------+-----------+------------------------------+------------+
        |    Field    |  Type  | Unique | Optional | Nillable | Default | UpdateDefault | Immutable |          StructTag           | Validators |
        +-------------+--------+--------+----------+----------+---------+---------------+-----------+------------------------------+------------+
        | id          | int    | false  | false    | false    | false   | false         | false     | json:"id,omitempty"          |          0 |
        | description | string | false  | false    | false    | false   | false         | false     | json:"description,omitempty" |          0 |
        +-------------+--------+--------+----------+----------+---------+---------------+-----------+------------------------------+------------+
        +-------+------+---------+------------------+----------+--------+----------+
        | Edge  | Type | Inverse |     BackRef      | Relation | Unique | Optional |
        +-------+------+---------+------------------+----------+--------+----------+
        | owner | Item | true    | item_description | O2O      | true   | false    |
        +-------+------+---------+------------------+----------+--------+----------+

ItemGroup:
        +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+
        | Field |  Type  | Unique | Optional | Nillable | Default | UpdateDefault | Immutable |       StructTag       | Validators |
        +-------+--------+--------+----------+----------+---------+---------------+-----------+-----------------------+------------+
        | id    | int    | 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 |
        +------------+------+---------+------------+----------+--------+----------+
        | group_item | Item | true    | item_group | M2M      | false  | true     |
        +------------+------+---------+------------+----------+--------+----------+

ItemVariation:
        +--------------+--------+--------+----------+----------+---------+---------------+-----------+-------------------------------+------------+
        |    Field     |  Type  | Unique | Optional | Nillable | Default | UpdateDefault | Immutable |           StructTag           | Validators |
        +--------------+--------+--------+----------+----------+---------+---------------+-----------+-------------------------------+------------+
        | id           | int    | false  | false    | false    | false   | false         | false     | json:"id,omitempty"           |          0 |
        | variant_name | string | false  | false    | false    | false   | false         | false     | json:"variant_name,omitempty" |          0 |
        +--------------+--------+--------+----------+----------+---------+---------------+-----------+-------------------------------+------------+
        +-------------+------+---------+----------------+----------+--------+----------+
        |    Edge     | Type | Inverse |    BackRef     | Relation | Unique | Optional |
        +-------------+------+---------+----------------+----------+--------+----------+
        | origin_item | Item | true    | item_variation | M2O      | true   | false    |
        +-------------+------+---------+----------------+----------+--------+----------+

EdgesのRelationを見るとO2O(1:1), O2M(1:n), M2M(n:n) などになっていることが確認できます。

migration実行と作成されるtable

shcema定義はできたので、続いてdb migrationをしてみます。
migrationはこちらのcode client.Schema.Create()で簡単に行うことができます。

下記のようにtableが作成されるはずなので、次に実際に作成されたtableを確認してみます。
今回の例ではPostgreSQLを使用しています。

作成されたtableの構成を見てみます

\d
                   List of relations
 Schema |           Name           |   Type   |  Owner
--------+--------------------------+----------+---------
 public | item_descriptions        | table    | enttest
 public | item_descriptions_id_seq | sequence | enttest
 public | item_groups              | table    | enttest
 public | item_groups_id_seq       | sequence | enttest
 public | item_item_group          | table    | enttest
 public | item_variations          | table    | enttest
 public | item_variations_id_seq   | sequence | enttest
 public | items                    | table    | enttest
 public | items_id_seq             | sequence | enttest
  • items
\d items
                                 Table "public.items"
 Column |       Type        | Collation | Nullable |             Default
--------+-------------------+-----------+----------+----------------------------------
 id     | bigint            |           | not null | generated by default as identity
 name   | character varying |           | not null |
Indexes:
    "items_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "item_descriptions" CONSTRAINT "item_descriptions_items_item_description" FOREIGN KEY (item_item_description) REFERENCES items(id) ON DELETE SET NULL
    TABLE "item_item_group" CONSTRAINT "item_item_group_item_id" FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE
    TABLE "item_variations" CONSTRAINT "item_variations_items_item_variation" FOREIGN KEY (item_item_variation) REFERENCES items(id) ON DELETE SET NULL
  • item_descriptions
 \d item_descriptions
                                  Table "public.item_descriptions"
        Column         |       Type        | Collation | Nullable |             Default
-----------------------+-------------------+-----------+----------+----------------------------------
 id                    | bigint            |           | not null | generated by default as identity
 description           | character varying |           | not null |
 item_item_description | bigint            |           |          |
Indexes:
    "item_descriptions_pkey" PRIMARY KEY, btree (id)
    "item_descriptions_item_item_description_key" UNIQUE CONSTRAINT, btree (item_item_description)
Foreign-key constraints:
    "item_descriptions_items_item_description" FOREIGN KEY (item_item_description) REFERENCES items(id) ON DELETE SET NULL
  • item_variations
\d item_variations
                                  Table "public.item_variations"
       Column        |       Type        | Collation | Nullable |             Default
---------------------+-------------------+-----------+----------+----------------------------------
 id                  | bigint            |           | not null | generated by default as identity
 variant_name        | character varying |           | not null |
 item_item_variation | bigint            |           |          |
Indexes:
    "item_variations_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "item_variations_items_item_variation" FOREIGN KEY (item_item_variation) REFERENCES items(id) ON DELETE SET NULL
  • item_groups
\d item_groups
                              Table "public.item_groups"
 Column |       Type        | Collation | Nullable |             Default
--------+-------------------+-----------+----------+----------------------------------
 id     | bigint            |           | not null | generated by default as identity
 name   | character varying |           | not null |
Indexes:
    "item_groups_pkey" PRIMARY KEY, btree (id)
Referenced by:
    TABLE "item_item_group" CONSTRAINT "item_item_group_item_group_id" FOREIGN KEY (item_group_id) REFERENCES item_groups(id) ON DELETE CASCADE

これらからわかることとしては、

Edegesで指定したものがFKとして指定されている

Fieldsで指定していないidカラムがPKとして設定されている

これはentの仕様のようです。idはent側で予約されていて、stringに変更,別名にも変更できますが、原則PKとして設定されるようです。そのため複合PKなどもできない模様。(間違っているかも…)

https://entgo.io/docs/schema-fields/#id-field

Fieldsで指定していないitem_descriptions.item_item_descriptionカラムが作成されている

item:item_descriptionの1:1の関連付けのためitem_item_descriptionというカラムが作成されていました。デフォルトでは{table}_{edege name}という名前で作成されるようです。StorageKey()で別名にすることもできるようです。(item_variations.item_item_variationも同様に作成されています)

shcema定義していないitem_item_groupというテーブルが作成されている

これはitems:item_groupのn:nを紐づけるために作成されたようです。

\d item_item_group
             Table "public.item_item_group"
    Column     |  Type  | Collation | Nullable | Default
---------------+--------+-----------+----------+---------
 item_id       | bigint |           | not null |
 item_group_id | bigint |           | not null |
Indexes:
    "item_item_group_pkey" PRIMARY KEY, btree (item_id, item_group_id)
Foreign-key constraints:
    "item_item_group_item_group_id" FOREIGN KEY (item_group_id) REFERENCES item_groups(id) ON DELETE CASCADE
    "item_item_group_item_id" FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE

上記のように実際のtableにはrelationを表現するため、schemaで定義した以外のカラム、テーブルが作成されることが確認できました(最初は完全に勘違いしていて上記のようなFKもschemaとして定義してしまってました…)

どのようなSQLが実行されるか

わたしの経験上ORMによって意図しないSQLが実行され、障害につながった経験があります。
そのためどのようなSQLが実行されるか確認してみます。

SQLログの出力には下記を使用させてもらいました。

https://github.com/simukti/sqldb-logger
zerolog.SetGlobalLevel(zerolog.InfoLevel)
loggerAdapter := zerologadapter.New(zerolog.New(zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false}))
db = sqldblogger.OpenDriver(dataSource, db.Driver(), loggerAdapter)

sample codeで実行した出力されたいくつかのログを確認していきます。

create系

i, err := client.Item.Create().
	SetName(faker.StringWithSize(10)).
	Save(ctx)


// QueryContext args=["3OFsw5XWJD"] conn_id=SIC8SWI6MAiYKe2a duration=2.93027 query="INSERT INTO \"items\" (\"name\") VALUES ($1) RETURNING \"id\""

こちらはシンプルなinsert

g, err := client.ItemGroup.Create().
	SetName("group: "+faker.StringWithSize(5)).
	AddGroupItem(i1, i2).
	Save(ctx)


// QueryContext args=["group: OawEq"] conn_id=SIC8SWI6MAiYKe2a duration=6.937385 query="INSERT INTO \"item_groups\" (\"name\") VALUES ($1) RETURNING \"id\""
// ExecContext args=[29,4,30,4] conn_id=SIC8SWI6MAiYKe2a duration=9.702607 query="INSERT INTO \"item_item_group\" (\"item_id\", \"item_group_id\") VALUES ($1, $2), ($3, $4)"

こちらはitem groupを作成 + i1, i2のitemをそのgroupに追加しているので、item_groupsに1row、item_item_groupに2row insertしています。

select系

items, err := client.Item.Query().
	Where(
		item.Name(i1.Name),
	).
	All(ctx)


// QueryContext args=["GVCOHwpW4Q"] conn_id=SIC8SWI6MAiYKe2a duration=3.872787 query="SELECT DISTINCT \"items\".\"id\", \"items\".\"name\" FROM \"items\" WHERE \"items\".\"name\" = $1"

こちらはシンプルなselect

itemDescriptions, err := client.Item.Query().
	Where(
		item.Name(i1.Name),
	).
	QueryItemDescription().
	All(ctx)


QueryContext args=["GVCOHwpW4Q"] conn_id=SIC8SWI6MAiYKe2a duration=11.661346 query="SELECT DISTINCT \"item_descriptions\".\"id\", \"item_descriptions\".\"description\" FROM \"item_descriptions\" JOIN (SELECT \"items\".\"id\" FROM \"items\" WHERE \"items\".\"name\" = $1) AS \"t1\" ON \"item_descriptions\".\"item_item_description\" = \"t1\".\"id\""

これはitem.nameにmatchするitemのitem_descriptionを取得しています。
joinをすることで取得していることがわかります。

GroupByItem, err := client.Item.Query().
	Where(
		item.Name(i1.Name),
	).
	QueryItemGroup().
	All(ctx)
	

QueryContext args=["GVCOHwpW4Q"] conn_id=SIC8SWI6MAiYKe2a duration=4.769666 query="SELECT DISTINCT \"item_groups\".\"id\", \"item_groups\".\"name\" FROM \"item_groups\" JOIN (SELECT \"item_item_group\".\"item_group_id\" FROM \"item_item_group\" JOIN (SELECT \"items\".\"id\" FROM \"items\" WHERE \"items\".\"name\" = $1) AS \"t1\" ON \"item_item_group\".\"item_id\" = \"t1\".\"id\") AS \"t1\" ON \"item_groups\".\"id\" = \"t1\".\"item_group_id\""

こちらはitem.nameにmatchするitemが属しているgroupを取得しています。
SELECT * from item_groups join (select id from item_item_group ~ join(select ~) というようにjoinして取得していました。graph構造的にはさらにつなげることもできるのでその際はさらにjoinが続いていくのかと思われます。

まとめ

entのEdgesのの挙動と実際に作成されるtable,実行されるSQLを確認してみました。
気になった点としては、

  • idの予約などあるため、すでに稼働しているserviceへの導入は厳しそう?
  • schema定義したもの以外のものがDBに作成される

自分の経験上、まずDBありきで考えてしまうので、あくまでentityとしての繋がりを定義するということが新鮮でした。
またもともとfacebookが開発していたということもありgraph構造、graphqlにはかなり相性が良さそうですね!