🗂

sakila databaseのschemaをentで可能な限り再現してみた

2022/05/24に公開

entでmysqlのサンプルデータベースであるsakilaのschemaをどこまで再現できるか、挑戦してみました。できたものは、githubリポジトリで公開しています。
https://github.com/nnabeyang/sakila-app-template

結果としては、ほぼ再現でき、テーブル定義としては完全にできなくても、Go言語と合わせればほぼ同等にできそうだという感想を持ちました。sakilaはentの標準の命名規則と当然ずれているので、ここではそこの調整方法について紹介してみます。

テーブル名の調整

例えばcustomerテーブルを作成しようとしてent init Customerしてテーブル生成するとテーブル名はcustomersになってしまいます。次のようにAnnotationsでMySQL上のテーブル名を指定することができます。

./ent/schema/customer.go
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")になります。

./ent/schema/customer.go
func (Customer) Fields() []ent.Field {
	return []ent.Field{
		field.Uint16("id"),
        ...
    }
}

MySQLの整数型のバイト数をリンク先で確認すれば、entでの指定の仕方は大体想像できると思います。
https://dev.mysql.com/doc/refman/8.0/ja/integer-types.html

プライマリーキーの名前の調整

プライマリーキーのMySQL上の名前を変更するにはStorageKeyで指定することができます。customerの例の場合、次のようにします。

./ent/schema/customer.go
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の場合だけ指定がある例になります。

./ent/schema/customer.go
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;

日時型の調整

日時を扱うtimestampdatetimeの時はfield.Timeで作れます。

日時型の型の指定

デフォルトがtimestampで、datetimeを使いたい場合は、文字列型の時と同様SchemaTypeで指定します。

./ent/schema/customer.go
func (Customer) Fields() []ent.Field {
	return []ent.Field{
        ...
		field.Time("create_date").SchemaType(map[string]string{
			dialect.MySQL: "datetime",
		}),
        ...
    }
}

Default値の指定

timestampのデフォルトにCURRENT_TIMESTAMPを指定したい場合は次のようにAnnotationsで指定します。

./ent/schema/customer.go
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_datelast_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.BytesBLOBに対応します。

その他の場合

geometryとかが良い例で、addressテーブルにあります。次のようにfield.Otherを使います。このような場合、sql.Scannerdriver.Valuerのインターフェイスを持つ型を作る必要があります。entのfaq.mdにもそのままの例が載っています。

./ent/schema/file.go
func (Address) Fields() []ent.Field {
	return []ent.Field{
        ...
		field.Other("location", &Point{}).
			SchemaType(Point{}.SchemaType()),
        ...
    }
}

中間テーブル

sakilaには中間テーブルとしてfilm_actorfilm_categoryがあります。entではこれらはEdgesを指定することで作成されます。
例えば次のようにすると、film_actorsという中間テーブルが作成されます。

./ent/schema/film.go
func (Film) Edges() []ent.Edge {
	return []ent.Edge{
        ...
		edge.To("actors", Actor.Type),
        ...
	}
}
./ent/schema/actor.go
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でテーブル名を指定できます。

./ent/schema/film.go
func (Film) Edges() []ent.Edge {
	return []ent.Edge{
        ...
        edge.To("actors", Actor.Type).StorageKey(edge.Table("film_actor"))
        ...
	}
}

インデックスの調整

ここではインデックス名や、タイプの指定方法を紹介します。

インデックス名の調整

名前はStorageKeyで指定できます。

./ent/schema/actor.go
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"),
	}
}

外部キー制約

中間テーブルの例のような特定な場合ではなく、外部キー制約だけ課したい場合は次のようにします。

./ent/schema/address.go
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にOnDeleteDefaultなどはありますが、OnUpdateはありませんでした。

year型

field.Uint16に型指定するとテーブル作成時は成功しますが、2度目の実行で失敗してしまいました。現在はただの整数型として扱っています。

FULLTEXT KEY制約

film_textに本来あったものですが、次のコードで作成しようとすると、失敗してしまいます。

./ent/schema/filmtext.go
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