GoのORM entのEdgesと生成されるTableとSQLなど
GoのORM entを触る機会がありドキュメントを斜め読みして、ははーんFieldsにテーブル構成書いて、Edgesにテーブル間の関係を書けば良きにはかられってくれるのかなと勝手に思っていたのが実際に触ってみるとけっこう違ったのでentのEdgesの挙動を確認してみたmemo
entとは
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とは
下記のような
- itemとitem descriptionは1:1
- itemとitem variationは1:n
- itemとitem categoryはn:n
のようなものです。
entのEdgesでは上記のようなentity間の関連をcodeで定義できます。それを元にqueryなどすることができます。
Edgesを定義する
これからは下記のsample codeを元に進めていきます。
上記の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として指定されている
id
カラムがPKとして設定されている
Fieldsで指定していないこれはentの仕様のようです。id
はent側で予約されていて、stringに変更,別名にも変更できますが、原則PKとして設定されるようです。そのため複合PKなどもできない模様。(間違っているかも…)
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が実行されるか確認してみます。
下記のようにDebug()を使うと実行されるSQLが出力されます
client.Debug().Item.Query()...
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にはかなり相性が良さそうですね!
Discussion