🔗

AtlasとEntでリレーションを設定する


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というエンティティ(テーブル)を作成して、それぞれのリレーションを作成する
    • parentedge.Tochildedge.Fromを設定する
  • リレーションの種類は、edge.Toedge.FromUniqueをつけるかどうかで変わる
    • 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にRequiredImmutableを追加する
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.Toedge.FromFieldメソッドを使用することで、外部キーカラムの名前を指定できる
  • 外部キーが実際に生成されないテーブル側で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で扱うフィールド名は変更されないため、特別な理由がない限り使用を避けるべき

その他

  • 関連テーブルを除き、一般的なテーブルでは複合主キーを作成できなそう
  • 主キーでないカラムに対し、外部キー制約を設定する手段はなさそう