sakila databaseのschemaをentで可能な限り再現してみた
entでmysqlのサンプルデータベースであるsakilaのschemaをどこまで再現できるか、挑戦してみました。できたものは、githubリポジトリで公開しています。
結果としては、ほぼ再現でき、テーブル定義としては完全にできなくても、Go言語と合わせればほぼ同等にできそうだという感想を持ちました。sakilaはentの標準の命名規則と当然ずれているので、ここではそこの調整方法について紹介してみます。
テーブル名の調整
例えばcustomerテーブルを作成しようとしてent init Customerしてテーブル生成するとテーブル名はcustomersになってしまいます。次のようにAnnotationsでMySQL上のテーブル名を指定することができます。
func (Customer) Annotations() []schema.Annotation {
return []schema.Annotation{
entsql.Annotation{Table: "customer"},
}
}
プライマリーキーの調整
entのプライマリーキーは、指定しなければidでMySQL上の型はbigintになります。一方sakilaのcustomerのプライマリーキーはcustomer_idで型はsamallint unsignedです。型、名前と順番に指定の仕方を紹介します。
プライマリーキーの型の調整
idの型を変更したい場合はfield.<型>("id")で型を変更できます。smallint unsignedは2バイトの符号なし整数なので、entだとfield.Uint16("id")になります。
func (Customer) Fields() []ent.Field {
return []ent.Field{
field.Uint16("id"),
...
}
}
MySQLの整数型のバイト数をリンク先で確認すれば、entでの指定の仕方は大体想像できると思います。
プライマリーキーの名前の調整
プライマリーキーのMySQL上の名前を変更するにはStorageKeyで指定することができます。customerの例の場合、次のようにします。
func (Customer) Fields() []ent.Field {
return []ent.Field{
field.Uint16("id").
StorageKey("customer_id"),
...
}
}
この箇所をDDLに翻訳すると、次のようになります。
CREATE TABLE `customer` (
`customer_id` smallint unsigned NOT NULL AUTO_INCREMENT,
...
PRIMARY KEY (`customer_id`),
...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
文字列型の調整
field.Stringを使うとvarchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULLになります。長さを指定したい場合は、次のようにSchemaTypeで指定します。RDBごとに異なる指定ができてdialect.<RDB>で指定できます。次のコードはMySQLの場合だけ指定がある例になります。
func (Customer) Fields() []ent.Field {
return []ent.Field{
...
field.String("first_name").
SchemaType(map[string]string{
dialect.MySQL: "varchar(45)",
}),
...
}
}
このコードに対応するDDLは次のようになります。
CREATE TABLE `customer` (
...
`first_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
...
PRIMARY KEY (`customer_id`),
...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
日時型の調整
日時を扱うtimestampやdatetimeの時はfield.Timeで作れます。
日時型の型の指定
デフォルトがtimestampで、datetimeを使いたい場合は、文字列型の時と同様SchemaTypeで指定します。
func (Customer) Fields() []ent.Field {
return []ent.Field{
...
field.Time("create_date").SchemaType(map[string]string{
dialect.MySQL: "datetime",
}),
...
}
}
Default値の指定
timestampのデフォルトにCURRENT_TIMESTAMPを指定したい場合は次のようにAnnotationsで指定します。
func (Customer) Fields() []ent.Field {
return []ent.Field{
...
field.Time("last_update").
Default(time.Now).
Annotations(&entsql.Annotation{
Default: "CURRENT_TIMESTAMP",
}).
UpdateDefault(time.Now),
...
}
}
Default(time.Now)はGo言語側の値の方の指定になります(CURRENT_TIMESTAMPが無くてもGo側での変更なら現在時刻が指定無しで入る)。
上記のように指定した時のcreate_dateとlast_updateの対応するDDLを書いておきます。
CREATE TABLE `customer` (
...
`create_date` datetime NOT NULL,
`last_update` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
...
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
残念ながらON UPDATE CURRENT_TIMESTAMPの指定の仕方は分かりませんでした。
バイナリ型
field.BytesがBLOBに対応します。
その他の場合
geometryとかが良い例で、addressテーブルにあります。次のようにfield.Otherを使います。このような場合、sql.Scanner、driver.Valuerのインターフェイスを持つ型を作る必要があります。entのfaq.mdにもそのままの例が載っています。
func (Address) Fields() []ent.Field {
return []ent.Field{
...
field.Other("location", &Point{}).
SchemaType(Point{}.SchemaType()),
...
}
}
中間テーブル
sakilaには中間テーブルとしてfilm_actorとfilm_categoryがあります。entではこれらはEdgesを指定することで作成されます。
例えば次のようにすると、film_actorsという中間テーブルが作成されます。
func (Film) Edges() []ent.Edge {
return []ent.Edge{
...
edge.To("actors", Actor.Type),
...
}
}
func (Actor) Edges() []ent.Edge {
return []ent.Edge{
edge.From("films", Film.Type).Ref("actors"),
}
}
作成されるテーブルの定義は次の通りです。
CREATE TABLE `film_actors` (
`film_id` smallint unsigned NOT NULL,
`actor_id` smallint unsigned NOT NULL,
PRIMARY KEY (`film_id`,`actor_id`),
KEY `film_actors_actor_id` (`actor_id`),
CONSTRAINT `film_actors_actor_id` FOREIGN KEY (`actor_id`) REFERENCES `actor` (`actor_id`) ON DELETE CASCADE,
CONSTRAINT `film_actors_film_id` FOREIGN KEY (`film_id`) REFERENCES `film` (`film_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
sakilaにはlast_updateがありますが、entで追加する方法は分かりませんでした。ですが、中間テーブルに更新日時が必要になる場面も無いように思うので、ここでは中間テーブル名だけ変えておわりにしましょう。
中間テーブルの名前の指定
今まで登場したStorageKeyとは別のものですが、中間テーブルもStorageKeyでテーブル名を指定できます。
func (Film) Edges() []ent.Edge {
return []ent.Edge{
...
edge.To("actors", Actor.Type).StorageKey(edge.Table("film_actor"))
...
}
}
インデックスの調整
ここではインデックス名や、タイプの指定方法を紹介します。
インデックス名の調整
名前はStorageKeyで指定できます。
func (Actor) Indexes() []ent.Index {
return []ent.Index{
index.Fields("last_name").
StorageKey("idx_actor_last_name"),
}
}
インデックスタイプの調整
インデックスタイプはAnnotationsでRDB毎に指定できます。
func (Address) Indexes() []ent.Index {
return []ent.Index{
...
index.Fields("location").
Annotations(
entsql.IndexTypes(map[string]string{
dialect.MySQL: "SPATIAL",
}),
).
StorageKey("idx_location"),
}
}
外部キー制約
中間テーブルの例のような特定な場合ではなく、外部キー制約だけ課したい場合は次のようにします。
func (Address) Edges() []ent.Edge {
return []ent.Edge{
edge.To("city", City.Type).
Field("city_id").
Unique().
Required().
StorageKey(edge.Symbol("fk_address_city")).
Annotations(&entsql.Annotation{
OnDelete: entsql.Restrict,
}),
}
}
上記、コードは次のような制約になります。
CREATE TABLE `address` (
...
CONSTRAINT `fk_address_city` FOREIGN KEY (`city_id`) REFERENCES `city` (`city_id`) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
できなかった事
ON UPDATEの指定
AnnotationsにOnDeleteやDefaultなどはありますが、OnUpdateはありませんでした。
year型
field.Uint16に型指定するとテーブル作成時は成功しますが、2度目の実行で失敗してしまいました。現在はただの整数型として扱っています。
FULLTEXT KEY制約
film_textに本来あったものですが、次のコードで作成しようとすると、失敗してしまいます。
func (FilmText) Indexes() []ent.Index {
return []ent.Index{
// FULLTEXT KEY `idx_title_description` (`title`, `description`)
index.Fields("title", "description").
Annotations(
entsql.IndexType("FULLTEXT"),
).
StorageKey("idx_title_description"),
}
}
failed creating schema resources: sql/schema: create index "idx_title_description": Error 1170: BLOB/TEXT column 'description' used in key specification without a key length
SET型
filmテーブルでSET型が使われていますが、作成方法が分かりませんでした。
Discussion