TL;DR
Unique
を使うことでリレーションを設定できる
- 主キーでないカラムにリレーションは設定できない
Required
制約はコンパイルでなく実行時エラーになるので注意が必要
きっかけ
- 宣言的にDBのテーブル定義ができるEntとAtlasを使っている
- Entとは、Go言語向けのORMであり、宣言したスキーマからORMコードを自動生成できる
- Atlasとは、DBスキーマ管理・マイグレーションツールであり、Entなどで定義したスキーマからDDLを生成できる
- 公式ドキュメントをみても細かな挙動がわからないので、実際に試してみた
環境
atlas: 0.35.0
postgres: 15.2
リレーション設定方法の調査
- O2O, O2M, M2Mのリレーションを設定してみる
parent
, child
というエンティティ(テーブル)を作成して、それぞれのリレーションを作成する
parent
にedge.To
、child
にedge.From
を設定する
- リレーションの種類は、
edge.To
やedge.From
にUnique
をつけるかどうかで変わる
edge.To
を設定することで、このエンティティから他のエンティティへの関連を定義し、外部キーカラムが作成される
edge.From
を設定することで、他のエンティティからこのエンティティへの逆方向の関連を定義できる
edge.To
の第一引数とedge.From
に設定するRef
は対応している必要がある
edge.From
の第一引数は関連の名前であり、goの生成コードに反映されるが、DDLには影響しない
O2O
- parent、childに
Unique
を設定する
- childテーブルに外部キーカラムが自動で作成される
type Parent struct {
ent.Schema
}
func (Parent) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Parent) Edges() []ent.Edge {
return []ent.Edge{
edge.To("relation1", Child.Type).Unique(), // <-- Uniqueをつける
}
}
type Child struct {
ent.Schema
}
func (Child) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Child) Edges() []ent.Edge {
return []ent.Edge{
edge.From("back_ref", Parent.Type).Ref("relation1").Unique(), // <-- Uniqueをつける
}
}
-- create "parents" table:
CREATE TABLE "parents" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" character varying NOT NULL,
PRIMARY KEY ("id")
);
-- create "childs" table:
CREATE TABLE "childs" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" character varying NOT NULL,
"parent_relation1" bigint NULL,
PRIMARY KEY ("id"),
CONSTRAINT "childs_parents_relation1" FOREIGN KEY ("parent_relation1") REFERENCES "parents" ("id") ON UPDATE NO ACTION ON DELETE SET NULL
);
-- create index "childs_parent_relation1_key" to table: "childs":
CREATE UNIQUE INDEX "childs_parent_relation1_key" ON "childs" ("parent_relation1");
O2M
- child側に
Unique
をつける
- childテーブルに外部キーカラムが作成される
- child側にFromを設定しなくても生成されるSQLは変わらない
type Parent struct {
ent.Schema
}
func (Parent) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Parent) Edges() []ent.Edge {
return []ent.Edge{
edge.To("relation1", Child.Type), // <-- Uniqueをつけない
}
}
type Child struct {
ent.Schema
}
func (Child) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Child) Edges() []ent.Edge {
return []ent.Edge{
// なくても生成されるDDL文は変わらない
// あるとWithBackRefでparentとchildを両方取得できるメソッドが生成される
edge.From("back_ref", Parent.Type).Ref("relation1").Unique(), // <-- Uniqueをつける
}
}
-- create "parents" table:
CREATE TABLE "parents" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" character varying NOT NULL,
PRIMARY KEY ("id")
);
-- create "childs" table:
CREATE TABLE "childs" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" character varying NOT NULL,
"parent_relation1" bigint NULL,
PRIMARY KEY ("id"),
CONSTRAINT "childs_parents_relation1" FOREIGN KEY ("parent_relation1") REFERENCES "parents" ("id") ON UPDATE NO ACTION ON DELETE SET NULL
);
補足
- 利用するケースはあまりないと思うが、上記とは逆にchildに
Unique
をつけても機能する
- parentテーブルに外部キーカラムが作成される
type Parent struct {
ent.Schema
}
func (Parent) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Parent) Edges() []ent.Edge {
return []ent.Edge{
edge.To("relation1", Child.Type).Unique(), // <-- Uniqueをつける
}
}
type Child struct {
ent.Schema
}
func (Child) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Child) Edges() []ent.Edge {
return []ent.Edge{
// なくても生成されるDDL文は変わらない
edge.From("back_ref", Parent.Type).Ref("relation1"), // <-- Uniqueをつけない
}
}
-- create "childs" table:
CREATE TABLE "childs" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" character varying NOT NULL,
PRIMARY KEY ("id")
);
-- create "parents" table:
CREATE TABLE "parents" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" character varying NOT NULL,
"parent_relation1" bigint NULL,
PRIMARY KEY ("id"),
CONSTRAINT "parents_childs_relation1" FOREIGN KEY ("parent_relation1") REFERENCES "childs" ("id") ON UPDATE NO ACTION ON DELETE SET NULL
);
M2M
- child、parentともに
Unique
をつけない
- 中間テーブルが作成される
type Parent struct {
ent.Schema
}
func (Parent) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Parent) Edges() []ent.Edge {
return []ent.Edge{
edge.To("relation1", Child.Type), // <-- Uniqueをつけない
}
}
type Child struct {
ent.Schema
}
func (Child) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Child) Edges() []ent.Edge {
return []ent.Edge{
edge.From("back_ref", Parent.Type).Ref("relation1"), // <-- Uniqueをつけない
}
}
-- create "childs" table:
CREATE TABLE "childs" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" character varying NOT NULL,
PRIMARY KEY ("id")
);
-- create "parents" table:
CREATE TABLE "parents" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" character varying NOT NULL,
PRIMARY KEY ("id")
);
-- create "parent_relation1" table:
CREATE TABLE "parent_relation1" (
"parent_id" bigint NOT NULL,
"child_id" bigint NOT NULL,
PRIMARY KEY ("parent_id", "child_id"),
CONSTRAINT "parent_relation1_child_id" FOREIGN KEY ("child_id") REFERENCES "childs" ("id") ON UPDATE NO ACTION ON DELETE CASCADE,
CONSTRAINT "parent_relation1_parent_id" FOREIGN KEY ("parent_id") REFERENCES "parents" ("id") ON UPDATE NO ACTION ON DELETE CASCADE
);
リレーション制約の調査
- 下記のO2Mの例で、childに
Required
、Immutable
を追加する
type Parent struct {
ent.Schema
}
func (Parent) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Parent) Edges() []ent.Edge {
return []ent.Edge{
edge.To("relation1", Child.Type),
}
}
type Child struct {
ent.Schema
}
func (Child) Fields() []ent.Field {
return []ent.Field{
field.String("name").NotEmpty(),
}
}
func (Child) Edges() []ent.Edge {
return []ent.Edge{
edge.From("back_ref", Parent.Type).Ref("relation1").Unique().XXX(), // <- ここに制約を追加する
}
}
Requiredを付与
- リレーションカラムに
not null
制約を追加される
- 生成されたentにおいて、Create時に
SetBackRef
を含めないと実行時エラーになる
ALTER TABLE "childs" DROP CONSTRAINT "childs_parents_relation1", ALTER COLUMN "parent_relation1" SET NOT NULL, ADD CONSTRAINT "childs_parents_relation1"
FOREIGN KEY ("parent_relation1") REFERENCES "parents" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;
child, err := client.Child.
Create().
SetName("new child").
SetBackRef(&ent.Parent{ // <-- ないと実行時エラー(missing required edge "Child.back_ref")
ID: 1,
Name: "new parent",
}).
Save(ctx)
Immutableを付与
- DDL文は変化なし
- データ変更不可となり、Update時にSetBackRefを設定できなくなる
child, err := client.Child.
UpdateOneID(3).
SetName("new child").
// SetBackRef(&ent.Parent{ // Immutable の場合はこのメソッドが作られない
// ID: 1,
// Name: "new parent",
//}).
Save(ctx)
- 補足
Immutable
を付与していない場合は、SetBackRef
を使うことで関連付けられているIDが更新できる
- ただし、そのほかのフィールド(例だと
Name: new parent
)は更新されない
命名方法の調査
- デフォルトではカラム名が自動で生成される
- これを任意のカラム名に変更する方法を調査する
Fieldを使う
edge.To
やedge.From
でField
メソッドを使用することで、外部キーカラムの名前を指定できる
- 外部キーが実際に生成されないテーブル側で
Field
を呼び出すと、スキーマ生成時にエラーになる
Error: loading ent schema: entc/gen: set "Parent" foreign-keys: field "ref_id" was not found in Child.Fields() for edge "back_ref"
StorageKeyを使う
- フィールド定義で
StorageKey
メソッドを使用することで、実際のデータベースのカラム名を設定できる
- これを使うことでidカラムの名前も変更可能
- ただし、Entで扱うフィールド名は変更されないため、特別な理由がない限り使用を避けるべき
その他
- 関連テーブルを除き、一般的なテーブルでは複合主キーを作成できなそう
- 主キーでないカラムに対し、外部キー制約を設定する手段はなさそう