在过去的几个月里,Ent 项目的 issues 中,关于在检索一对一或一对多边缘的实体时追加检索外键字段的讨论颇多。我们很高兴宣布,从 v0.7.0 开始,Ent 已支持此功能。
Edge-field 支持之前
在合并此分支之前,想要检索一个实体的外键字段的用户需要使用预加载。假设我们的架构如下所示:
// ent/schema/user.go:
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("name").
Unique().
NotEmpty(),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.From("pets", Pet.Type).
Ref("owner"),
}
}
// ent/schema/pet.go
// Pet holds the schema definition for the Pet entity.
type Pet struct {
ent.Schema
}
// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
}
}
// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Unique().
Required(),
}
}
该架构描述了两个关联实体:User 与 Pet,它们之间存在一对多边缘:一个用户可以拥有多只宠物,而一只宠物只能有一个所有者。
当从存储库检索宠物时,开发者通常需要访问宠物上的外键字段。然而,由于该字段是从 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()
// Create the User
u := c.User.Create().
SetUserName("rotem").
SaveX(ctx)
// Create the Pet
p := c.Pet.
Create().
SetOwner(u). // Associate with the 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)
// Output: 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 的宠物,然后冗余地从 users 表中获取 ID 为 1 的用户的 id 字段。
使用 Edge-field 支持
Edge-field 支持极大简化并提升了此流程的效率。通过此功能,开发者可以在架构的 Fields() 中定义外键字段,并在边缘定义时使用 .Field(..) 修饰符告诉 Ent 映射该列到此字段。因此,在我们的示例架构中,我们可以按如下方式修改:
// user.go stays the same
// pet.go
// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Int("owner_id"), // <-- explicitly add the field we want to contain the FK
}
}
// Edges of the Pet.
func (Pet) Edges() []ent.Edge {
return []ent.Edge{
edge.To("owner", User.Type).
Field("owner_id"). // <-- tell ent which field holds the reference to the owner
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) // <-- Simply retrieve the Pet
fmt.Println(petWithOwnerId.OwnerID)
// Output: 1
}
使用 .Debug() 修饰符运行时,可以看到 DB 查询现在更合理:
SELECT DISTINCT `pets`.`id`, `pets`.`name`, `pets`.`owner_id` FROM `pets` WHERE `pets`.`id` = ? LIMIT 2
太好了 🎉!
将现有架构迁移至 Edge-fields
如果你已经在使用现有架构的 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}, // <-- this is our FK
}
为了实现平滑迁移,你必须明确告诉 Ent 继续使用现有列名。你可以使用 StorageKey 修饰符(可以在字段或边缘上使用)来实现。例如:
// In schema/pet.go:
// Fields of the Pet.
func (Pet) Fields() []ent.Field {
return []ent.Field{
field.String("name").
NotEmpty(),
field.Int("owner_id").
StorageKey("pet_owner"), // <-- explicitly set the column name
}
}
在不久的将来,我们计划实现架构版本化,它将把架构更改的历史与代码存储在一起。拥有这些信息将使 ent 能够以自动且可预测的方式支持此类迁移。
总结
Edge-field 支持已准备就绪,可通过 go get -u entgo.io/ent@v0.7.0 安装。
感谢所有花时间提供反馈并帮助我们妥善设计此功能的优秀人士: Alex Snast,Ruben de Vries,Marwan Sulaiman,Andy Day,Sebastian Fekete 和 Joe Harvey。
更多 Ent 动态与更新:
- 在 twitter.com/entgo_io 上关注我们
- 订阅我们的 newsletter
- 加入我们在 #ent 的 Gophers Slack
- 加入我们的 Ent Discord 服务器