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