メインコンテンツへスキップする

v0.7.0でのエッジフィールドのサポートをお知らせします

· 1 分で読む

この数ヶ月間、Entプロジェクトのissuesでは、One-to-OneまたはOne-to-Manyのエッジを持つエンティティを検索する際に、外部キーフィールドの検索サポートを追加することについて多くの議論がなされました。 v0.7.0からentがこの機能をサポートしたことをお知らせします。

エッジフィールドのサポート以前

このブランチがマージされる前は、エンティティの foreign-key フィールドを取得したいユーザーは、eager-loadingを使用する必要がありました。 スキーマは以下のようになります。

// ent/schema/user.go:

// Userは、Userエンティティのスキーマ定義を保持しています。
type User struct {
ent.Schema
}

// Userのフィールド
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Unique().
NotEmpty(),
}
}

// Userのエッジ
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("pets", Pet.Type).
Ref("owner"),
}
}

// ent/schema/pet.go

// Petは、Petエンティティのスキーマ定義を保持しています。
type Pet struct {
ent.Schema
}

// Petのフィールド
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
}
}

// Petのエッジ
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Unique().
Required(),
}
}

このスキーマは2つの関連するエンティティを記述しています。UserPet、両者の間には一対多のエッジがあります。ユーザーは多くのペットを所有することができ、ペットは1人の飼い主を持つことができます。

データストレージからペットを取得する場合、開発者はペットの外部キーフィールドにアクセスしたいと考えるのが一般的です。 しかし、このフィールドはownerエッジから暗黙的に作成されるため、エンティティを取得する際に自動的にアクセスできました。 これをストレージから取り出すには、開発者は次のような作業が必要です。

func Test(t *testing.T) {
ctx := context.Background()
c := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
defer c.Close()

// Userを作成する
u := c.User.Create().
SetUserName("rotem").
SaveX(ctx)

// Petを作成する
p := c.Pet.
Create().
SetOwner(u). // PetをUserに関連付ける
SetName("donut").
SaveX(ctx)

petWithOwnerId := c.Pet.Query().
Where(pet.ID(p.ID)).
WithOwner(func(query *ent.UserQuery) {
query.Select(user.FieldID)
}).
OnlyX(ctx)
fmt.Println(petWithOwnerId.Edges.Owner.ID)
// 出力: 1
}

非常に冗長であるだけでなく、この方法でペットと飼い主を検索することは、データベースのクエリとしては非効率的でした。 このクエリを.Debug()で実行すると、この呼び出しを満たすためにentが生成するDBクエリを見ることができます。

SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`pet_owner` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2 
SELECT DISTINCT `users`.`id` FROM `users` WHERE `users`.`id` IN (?)

この例では、EntはまずIDが1のPetを取得し、次にIDが1のユーザーに対してusersテーブルからidフィールドを冗長的に取得します。

エッジフィールドのサポートあり

エッジフィールドサポートは、このフローを大幅に簡素化し、効率化します。 この機能により、開発者はスキーマのFields()の一部として外部キーフィールドを定義し、エッジ定義の.Field(..)修飾子を使用して、Entに外部カラムをこのフィールドに公開してマッピングするように指示することができます。 そこで、今回のスキーマ例では、次のように修正します。

// user.go は同様のまま

// pet.go
// Petのフィールド
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Int("owner_id"), // <-- FKを入れたいフィールドを明示的に追加する
}
}

// Petのエッジ
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Field("owner_id"). // <-- どのフィールドが飼い主への参照を保持しているのかをentに伝える
Unique().
Required(),
}
}

クライアントコードを更新するには、以下のようにコード生成を再実行する必要があります。

go generate ./...

これで、クエリをもっとシンプルなものに変更することができます。

func Test(t *testing.T) {
ctx := context.Background()
c := enttest.Open(t, dialect.SQLite, "file:ent?mode=memory&cache=shared&_fk=1")
defer c.Close()

u := c.User.Create().
SetUserName("rotem").
SaveX(ctx)

p := c.Pet.Create().
SetOwner(u).
SetName("donut").
SaveX(ctx)

petWithOwnerId := c.Pet.GetX(ctx, p.ID) // <-- ペットを取り出すだけ

fmt.Println(petWithOwnerId.OwnerID)
// 出力: 1
}

.Debug()修飾子を使って実行すると、DBのクエリがよりセンスあるものになったことがわかります。

SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`owner_id` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2

ヨシッ 🎉!

既存のスキーマをエッジフィールドに移行する

既に既存のスキーマでEntを使用している場合、外部キーのカラムを持つO2Mリレーションが既にデータベースに存在している可能性があります。 スキーマの構成によっては、今から追加するフィールドとは別の名前のカラムに格納されている可能性があります。 例えば、owner_idフィールドを作成したいのに、Entはすでに外部キーのカラムをpet_ownerとして自動作成しているとします。

Entがこのフィールドに使用しているカラム名を確認するには、./ent/migrate/schema.goファイルを参照してください。

PetsColumns = []*schema.Column{
{Name: "id", Type: field.TypeInt, Increment: true},
{Name: "name", Type: field.TypeString},
{Name: "pet_owner", Type: field.TypeInt, Nullable: true}, // <-- これがFKです
}

スムーズな移行を可能にするためには、既存のカラム名を使い続けるようにEntに明示的に伝える必要があります。 StorageKey 修飾子 (フィールドまたはエッジのいずれか) を使用して、これを行うことができます。 以下の例をご覧ください。

// In schema/pet.go:

// Petのフィールド
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Int("owner_id").
StorageKey("pet_owner"), // <-- カラム名を明示的に設定する
}
}

近い将来、スキーマバージョニングを実装し、コードと一緒にスキーマの変更履歴を保存することを計画しています。 この情報があれば、entは自動的かつ予測可能な方法で、このような移行をサポートすることができます。

まとめ

エッジフィールドのサポートはすぐに利用できます。go get -u entgo.io/ent@v0.7.0でインストールしてください。

フィードバックに時間を割き、この機能を適切に設計するために協力してくれた善良な人々に多くの感謝🙏を捧げます。Alex SnastRuben de VriesMarwan SulaimanAndy DaySebastian FeketeJoe Harveyの皆さんです。

より多くのEntのニュースと最新情報をお届けします