go语言ORM框架ent使用教程

这篇文章是关于 Go 语言 ORM 框架 ent 的使用教程。首先介绍了 ent 的基本概念和安装,包括创建 schema、生成代码、连接数据库。然后详细说明了 CRUD 操作,如创建、批量创建、条件查询、模糊查询、条件更新和条件删除数据。最后讲解了 ent 进阶,包括联表查询中的 O2M、O2O、M2M 关系的定义和查询方式。

关联问题:ent支持哪些数据库?ent性能如何优化?ent更新数据复杂吗?
 
 

ent是什么

ent是一个简单而又功能强大的Go语言实体框架,ent易于构建和维护应用程序与大数据模型。

简而言之,ent是一款便于操作的orm框架

installation

go get entgo.io/ent/cmd/ent

使用

创建schema

在连接ent之前,我们首先需要创建schema,创建schema的作用类似django创建model,规定数据表字段,定义表名等

cli创建model模板命令

 
shell
代码解读
复制代码
ent init --target <target dirpath> <Model Name>

--target目的是指定创建模板路径, Model Name必须使用驼峰命名法

我们可以通过ent init --target spec/schema Class Student命令在spec/schema目录下创建两个模板,class.go和student.go

如下所示:

 
go
代码解读
复制代码
// spec/schema/class.go

package schema

import (
	"entgo.io/ent"
)

// Class holds the schema definition for the Class entity.
type Class struct {
	ent.Schema
}

// Fields of the Class.
func (Class) Fields() []ent.Field {
	return nil
}

// Edges of the Class.
func (Class) Edges() []ent.Edge {
	return nil
}
 
go
代码解读
复制代码
// spec/schema/student.go

package schema

import (
	"entgo.io/ent"
)

// Student holds the schema definition for the Student entity.
type Student struct {
	ent.Schema
}

// Fields of the Student.
func (Student) Fields() []ent.Field {
	return nil
}

// Edges of the Student.
func (Student) Edges() []ent.Edge {
	return nil
}

创建完整的数据结构

我们在生成的模板内,将表字段添加进去

 
go
代码解读
复制代码
// spec/schema/class.go

package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/dialect/entsql"
	"entgo.io/ent/schema"
	"entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
)

// Class holds the schema definition for the Class entity.
type Class struct {
	ent.Schema
}

// Fields of the Class.
func (Class) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").MaxLen(50).Comment("名称"),
		field.Int("level").Comment("级别"),
	}
}

// Edges of the Class.
func (Class) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("student", Student.Type),
	}
}

func (Class) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entsql.Annotation{Table: "class"},
	}
}
 
go
代码解读
复制代码
// spec/schema/student.go

package schema

import (
	"entgo.io/ent"
	"entgo.io/ent/dialect/entsql"
	"entgo.io/ent/schema"
	"entgo.io/ent/schema/edge"
	"entgo.io/ent/schema/field"
)

// Student holds the schema definition for the Student entity.
type Student struct {
	ent.Schema
}

// Fields of the Student.
func (Student) Fields() []ent.Field {
	return []ent.Field{
		field.String("name").MaxLen(50).Comment("名称"),
		field.Bool("sex").Comment("性别"),
		field.Int("age").Comment("年龄"),
		field.Int("class_id").Comment("班级ID"),
	}
}

// Edges of the Student.
func (Student) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("class", Class.Type).
			Ref("student").
			Unique().
			Field("class_id").
			Required(),
	}
}

func (Student) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entsql.Annotation{
			Table:       "student",
		},
	}
}

生成代码

基于schema的定义(字段,索引,边,配置)来生成对应的数据表操作代码

ent generate --target <target dirpath> <template dirpath>

template dirpath目的是指定模板所在路径,根据所在目录下的模板来生成对应的代码

--target <target dirpath?目的是指定生成操作代码的路径

使用ent generate --target gen/entschema spec/schema将我们的模板生成实际的操作代码到gen/entschema路径下

ent连接数据库

之前我们在gen/entschema目录下生成了操作代码,可以看到在其中有一个client.go的文件,它就是我们连接数据库的操作文件,我们需要先创建模型生成代码之后再连接数据库的原因也在于此

常规方式连接数据库

 
go
代码解读
复制代码
// main.go

package main

import (
    "context"
    "log"

    "<project>/gen/entschema"
)

func main() {
    // url example: username:password@(ipAddress)/databaseName?charset=utf8ma4&parseTime=true
    URL := "root:123456@(127.0.0.1)/test?charset=utf8mb4&parseTime=true"
    
    client, err := entschema.Open("mysql", URL)
    if err != nil {
        log.Fatalf("failed opening connection to sqlite: %v", err)
    }
    defer client.Close()
    // Run the auto migration tool.
    if err := client.Schema.Create(context.Background()); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
    }
}

整合使用sql.DB的方式连接数据库

 
go
代码解读
复制代码
// main.go

package main

import (
    "context"
    "log"
    "database/sql"

	_ "github.com/go-sql-driver/mysql" // 必要导入
    entsql "entgo.io/ent/dialect/sql"
    "<project>/gen/entschema"
)

func main() {
    // URL example: username:password@(ipAddress)/databaseName?charset=utf8ma4&parseTime=true
    URL := "root:123456@(127.0.0.1)/test?charset=utf8mb4&parseTime=true"
    
    var db *sql.DB
	db, err := sql.Open("mysql", URL)

	if err != nil {
		return err
	}
	db.SetMaxOpenConns(100)
	db.SetMaxIdleConns(50)
	drv := entsql.OpenDB("mysql", db)
	client = entschema.NewClient(schema.Driver(drv))

    defer client.Close()
    // Run the auto migration tool.
    if err := client.Schema.Create(context.Background()); err != nil {
        log.Fatalf("failed creating schema resources: %v", err)
    }
}

CRUD

上面介绍了数据库的连接,在实际使用中,我们往往并非在一个文件中编写所有的逻辑,所以需要将数据库的连接->client放在全局变量中

 
go
代码解读
复制代码
// app/app.go

package app

import (
	"database/sql"
	entsql "entgo.io/ent/dialect/sql"
	schema "goZeroApp/zerodemo/gen/entschema"
	"goZeroApp/zerodemo/internal/config"
)

var EntClient *schema.Client

func InitExtensions(URL string) error {
	var db *sql.DB
	db, err := sql.Open("mysql", URL)

	if err != nil {
		return err
	}
	db.SetMaxOpenConns(100)
	db.SetMaxIdleConns(50)
	drv := entsql.OpenDB("mysql", db)
	EntClient = schema.NewClient(schema.Driver(drv))
	return nil
}

我们只需要在main方法中运行的时候执行InitExtensions方法就可以在之后的操作中使用app.EntClient来获取到数据库的client

创建数据

以之前我们创建的Class和Student表为例,我们可以在任意文件中使用如下代码

 
go
代码解读
复制代码
package globle

import (
	"context"
	"<package>/app"
)

ctx := context.Background()
classObj, err := app.EntClient.Class.Create().SetName("三班").SetLevel(1).Save(ctx)

studentObj := app.EntClient.Student.Create().
	SetClass(classObj).
	SetName("小红").
	SetSex(false).
	SetAge(12).
	SaveX(ctx)

可以看到在保存的时候我们有两种方式Save()SaveXSaveX()内部实际上也是调用的Save(),但区别在于SaveX()不会返回error类型,通常我们只会在单元测试用使用带X的方法以保证我们的业务代码更加的健壮。

另外,由于我们在创建模型的时候使用了边(edge)为student和class创建了外键关联关系,因此我们可以在创建Student对象的时候使用SetClass(classObj)来关联Class对象

批量创建数据

 
go
代码解读
复制代码
package globle

import (
    "context"
    "<package>/app"
)

type studentData struct {
    Name string
    Age  int
    Sex  bool
}

data := make([]studentData, 3)

data[0].Name = "小明"
data[1].Name = "小刚"
data[2].Name = "小李"

data[0].Age = 12
data[1].Age = 13
data[2].Age = 11

data[0].Sex = true
data[1].Sex = true
data[2].Sex = false

bulk := make([]*entschema.StudentCreate, len(data))
for i, d := range data{
	bulk[i] := app.EntClient.Student.Create().SetName(d.Name).SetSex(d.Sex).SetAge(d.Age)
}

students, err := app.EntClient.Student.CreateBulk(bulk...).Save(ctx)

Create()的对象不立马保存,而是储存在切片中,之后再统一使用CreateBulk()的方法来批量创建数据

条件查询数据

 
go
代码解读
复制代码
package globle

import (
    "context"
    "<package>/app"
    "<package>/gen/entschema/student"
)

ctx := context.Background()
studentObj, err := app.EntClient.Student.Query().Where(student.Age(12)).First(ctx)
classObj := studentObj.QueryClass().FirstX(ctx)

FirstX()First()的关系和SaveX()Save()的关系相似,一个不返回error类型,一个则会返回它

在外键关联的情况下,我们可以通过Query后面接实体类名称的方式来关联查询另一张表的数据,我们可以在后面接Query()方法后面能接的所有方法

模糊查询数据

 
go
代码解读
复制代码
package globle

import (
    "context"
    "<package>/app"
    "<package>/gen/entschema/student"
    
    "entgo.io/ent/dialect/sql"
)

ctx := context.Background()
studentObj, err := app.EntClient.Student.Query().
	Where(func(selector *sql.Selector) {
	    selector.Where(sql.Like(selector.C(student.FieldName), "%红%"))
	}).
	First(ctx)
classObj := studentObj.QueryClass().FirstX(ctx)

可以通过Where方法中使用sql.Selector函数来间接达成模糊查询的作用

条件更新数据

 
go
代码解读
复制代码
package globle

import (
	"context"
	"<package>/app"
	"<package>/gen/entschema/student"
)

ctx := context.Background()
classObj, err := app.EntClient.Class.Create().SetName("一班").SetLevel(2).Save(context.Background())
updateCount, err := app.EntClient.Student.Update().Where(student.Name("小红")).SetAge(13).SetClass(classObj).Save(ctx)

假如时间过了一年,小红年龄增长一岁,并且年纪从一年级到了二年级,并且班级由三班变为一班,我们可以通过Update()Where()方法来条件修改她的属性,同时由于有外键关联关系,所以我们可以使用Set后面跟Class对象名的方式来更新class表数据的变更

条件删除数据

 
go
代码解读
复制代码
package globle

import (
	"context"
	"<package>/app"
	"<package>/gen/entschema/student"
)

ctx := context.Background()
deleteCount, err = app.EntClient.Student.Delete().Where(student.Name("小红")).Exec(ctx)

我们可以通过Delete()Where()方法来条件删除一些数据,方法与上面的例子类似

ent 进阶

在了解基本的CRUD操作之后我们还需要了解更多高级的sql操作接下来,我们将介绍联表查询,条件拼接查询以及在ent中直接使用sql查询等。

联表查询

在ent中,想要进行联表查询我们首先需要给我们对应的表之间定义Edge。通过Edge可以绑定实体之间的关系,以此实现联表查询。

O2M(一对多查询)

UserOrder举例,一个用户可以有多个订单的场景下我们应该如何定义模型

 
go
代码解读
复制代码
// spec/schema/user.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// 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").MaxLen(50).Comment("名称"),
    }
}

// Edges of the User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("orders", Order.Type),
    }
}

func (User) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entsql.Annotation{Table: "user"},
    }
}
 
go
代码解读
复制代码
// spec/schema/order.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// Order holds the schema definition for the Order entity.
type Order struct {
    ent.Schema
}

// Fields of the Order.
func (Order) Fields() []ent.Field {
    return []ent.Field{
	    ...
        field.String("product").MaxLen(50).Comment("产品"),
        field.Int("user_id").Comment("用户ID"),
    }
}

// Edges of the Order.
func (Order) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("user", User.Type).Ref("orders").Unique().Field("user_id").Required(),
    }
}

func (Order) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entsql.Annotation{Table: "order"},
    }
}

可以看到,在这个例子中,我们使用 edge.To 函数来定义 User 和 Order 实体之间的一对多关系。在 User 实体中,我们使用 edge.To 函数来定义一个名为 "orders" 的 edge,表示一个用户可以有多个订单。在 Order 实体中,我们使用 edge.From 函数来定义一个名为 "user" 的 edge,表示一个订单只属于一个用户

当我们想要进行联表查询时就可以进行如下查询了:

 
go
代码解读
复制代码
ctx := context.Background()

// 查询user下的所有orders
userID := 1
orders, _ := app.EntClient.User.Query().Where(user.ID(userID)).QueryOrder().All(ctx)

// 查询user,同时获取到user下的orders列表
user, _ := app.EntClient.User.Query().Where(user.ID(userID)).WithOrder().Only(ctx)
orders := user.Edges.Orders

// 查询user,同时获取到user下符合条件的orders列表
user, _ := app.EntClient.User.Query().Where(user.ID(userID)).WithOrder().Where(order.Product("xxx")).Only(ctx)
orders := user.Edges.Orders

// 查询order对应的user
orderID := 1
user, _ := app.EntClient.Order.Query().Where(order.ID(orderID)).QueryUser().Only(ctx)

// 查询order,同时获取order对应的user信息
order, _ := app.EntClient.Order.Query().Where(order.ID(orderID)).WithUser().Only(ctx)
user := order.Edges.User

O2O(一对一查询)

UserProfile举例,一个用户只拥有一份个人资料的情况下应该如何定义模型

 
go
代码解读
复制代码
// spec/schema/user.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// 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").MaxLen(50).Comment("名称"),
    }
}

// Edges of the User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("profile", Profile.Type).Unique(),
    }
}

func (User) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entsql.Annotation{Table: "user"},
    }
}
 
go
代码解读
复制代码
// spec/schema/profile.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// Profile holds the schema definition for the Profile entity.
type Profile struct {
    ent.Schema
}

// Fields of the Profile.
func (Profile) Fields() []ent.Field {
    return []ent.Field{
    	...
        field.String("bio").MaxLen(200).Comment("简介"),
        field.Int("user_id").Comment("用户ID"),
    }
}

// Edges of the Profile.
func (Profile) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("user", User.Type).Ref("profile").Unique().Field("user_id").Required(),
    }
}

func (Profile) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entsql.Annotation{Table: "profile"},
    }
}

可以看到,在这个例子中,我们使用 edge.To 函数来定义 User 和 Profile 实体之间的一对一关系。在 User 实体中,我们使用 edge.To 函数来定义一个名为 "profile" 的 edge,表示一个用户只能有一个个人资料。在 Profile 实体中,我们使用 edge.From 函数来定义一个名为 "user" 的 edge

例子举到这里,或许会有人发现,edge 的 To 和 From非常奇怪。我们在O2M的例子中,用 edge.To表示user可以有多个orderedge.From表示一个order只归属于一个user。为什么到了O2O的列子中,依旧需要用到ToFrom来定义关系?而到底哪个才应该是To来表示哪个应该用From表示?

原因在于在ent框架中,edge.Toedge.From的使用并不完全取决于关系是一对多(O2M)还是一对一(O2O)。它们主要是用来定义边缘的方向,即数据关系的指向。

  • edge.To用来表示从当前模型指向另一个模型。例如,在User模型中,edge.To("orders", Order.Type)表示从User指向Order,即一个用户可以有多个订单。

  • edge.From用来表示从另一个模型指向当前模型。例如,在Order模型中,edge.From("user", User.Type).Ref("orders").Unique().Field("user_id").Required()表示从Order指向User,即一个订单只能归属于一个用户。

在一对一(O2O)的关系中,我们依然需要使用edge.Toedge.From来定义边缘的方向。例如,在UserProfile的例子中,我们在User模型中使用edge.To("profile", Profile.Type).Unique()来表示从User指向Profile,即一个用户只能有一个个人资料。同时,我们在Profile模型中使用edge.From("user", User.Type).Ref("profile").Unique().Field("user_id").Required()来表示从Profile指向User,即一个个人资料只能归属于一个用户。

总的来说,edge.Toedge.From的使用取决于你想要如何定义数据关系的指向,而不仅仅是关系的类型(一对多或一对一)

O2O的联表查询和O2M的查询方式基本一致,这里就不再赘述。

M2M(多对多查询)

UserGroup举例,一个用户可以存在多个用户组中,一个用户组也可以包含多个用户。这里根据实际场景我们有两种处理方式

方案一 我们需要自定义UserGroup中间表

 
go
代码解读
复制代码
// spec/schema/user.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// AuthUser holds the schema definition for the AuthUser entity.
type User struct {
	ent.Schema
}

// Fields of the AuthUser.
func (User) Fields() []ent.Field {
	return []ent.Field{
		field.Int64("id").Unique().Comment("主键ID"),
		field.String("username").MaxLen(LenDesc).NotEmpty().Comment("用户名"),
	}
}

// Edges of the AuthUser.
func (User) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("groups", AuthGroup.Type).Through("user_groups", UserGroup.Type),
	}
}

func (User) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entsql.Annotation{Table: "user"},
	}
}
 
go
代码解读
复制代码
// spec/schema/group.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// AuthGroup holds the schema definition for the AuthGroup entity.
type Group struct {
	ent.Schema
}

// Fields of the AuthGroup.
func (Group) Fields() []ent.Field {
	return []ent.Field{
		field.Int64("id").Unique().Comment("主键ID"),
		field.String("name").MaxLen(LenDesc).NotEmpty().Comment("用户组名称"),
		field.String("description").Default("").Comment("描述"),
	}
}

// Edges of the AuthGroup.
func (Group) Edges() []ent.Edge {
	return []ent.Edge{
		edge.From("users", AuthUser.Type).Ref("groups").Through("user_groups", UserGroup.Type),
	}
}

func (Group) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entsql.Annotation{Table: "group"},
	}
}
 
go
代码解读
复制代码
// spec/schema/usergroup.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// AuthUserGroup holds the schema definition for the AuthUserGroup entity.
type UserGroup struct {
	ent.Schema
}

// Fields of the AuthUserGroup.
func (UserGroup) Fields() []ent.Field {
	return []ent.Field{
		field.Int64("id").Unique().Comment("主键ID"),
		field.Int64("user_id").Comment("用户ID"),
		field.Int64("group_id").Comment("用户组ID"),
	}
}

// Edges of the AuthUserGroup.
func (UserGroup) Edges() []ent.Edge {
	return []ent.Edge{
		edge.To("user", AuthUser.Type).
			Unique().
			Required().
			Field("user_id").
			Comment("用户ID"),
		edge.To("group", AuthGroup.Type).
			Unique().
			Required().
			Field("group_id").
			Comment("用户组ID"),
	}
}

func (UserGroup) Annotations() []schema.Annotation {
	return []schema.Annotation{
		entsql.Annotation{Table: "user_group"},
	}
}

方案二 我们选择ent为我们生成中间表

 
go
代码解读
复制代码
// spec/schema/user.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// 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").MaxLen(50).Comment("名称"),
    }
}

// Edges of the User.
func (User) Edges() []ent.Edge {
    return []ent.Edge{
        edge.To("groups", Group.Type).StorageKey(edge.Column("user_id")),
    }
}

func (User) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entsql.Annotation{Table: "user"},
    }
}
 
go
代码解读
复制代码
// spec/schema/group.go
package schema

import (
    "entgo.io/ent"
    "entgo.io/ent/dialect/entsql"
    "entgo.io/ent/schema/edge"
    "entgo.io/ent/schema/field"
)

// Group holds the schema definition for the Group entity.
type Group struct {
    ent.Schema
}

// Fields of the Group.
func (Group) Fields() []ent.Field {
    return []ent.Field{
        field.String("name").MaxLen(50).Comment("名称"),
    }
}

// Edges of the Group.
func (Group) Edges() []ent.Edge {
    return []ent.Edge{
        edge.From("users", User.Type).Ref("groups").StorageKey(edge.Column("group_id")),
    }
}

func (Group) Annotations() []schema.Annotation {
    return []schema.Annotation{
        entsql.Annotation{Table: "group"},
    }
}

以上两种方案仅仅是在edge的写法略有不同,对于实际查询方式并没有任何区别,而查询的方式和O2M的方式类似,这里也不再赘述,大家可以下来自己尝试一

posted @ 2024-12-10 12:55  技术颜良  阅读(7)  评论(0编辑  收藏  举报