go语言ORM框架ent使用教程
ent是什么
ent是一个简单而又功能强大的Go语言实体框架,ent易于构建和维护应用程序与大数据模型。
简而言之,ent是一款便于操作的orm框架
installation
go get entgo.io/ent/cmd/ent
使用
创建schema
在连接ent之前,我们首先需要创建schema,创建schema的作用类似django创建model,规定数据表字段,定义表名等
cli创建model模板命令
ent init --target <target dirpath> <Model Name>
--target
目的是指定创建模板路径, Model Name
必须使用驼峰命名法
我们可以通过ent init --target spec/schema Class Student
命令在spec/schema
目录下创建两个模板,class.go和student.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
}
// 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
}
创建完整的数据结构
我们在生成的模板内,将表字段添加进去
// 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"},
}
}
// 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的文件,它就是我们连接数据库的操作文件,我们需要先创建模型生成代码之后再连接数据库的原因也在于此
常规方式连接数据库
// 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的方式连接数据库
// 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放在全局变量中
// 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表为例,我们可以在任意文件中使用如下代码
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()
和SaveX
,SaveX()
内部实际上也是调用的Save()
,但区别在于SaveX()
不会返回error
类型,通常我们只会在单元测试用使用带X
的方法以保证我们的业务代码更加的健壮。
另外,由于我们在创建模型的时候使用了边(edge)为student和class创建了外键关联关系,因此我们可以在创建Student
对象的时候使用SetClass(classObj)
来关联Class
对象
批量创建数据
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()
的方法来批量创建数据
条件查询数据
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()
方法后面能接的所有方法
模糊查询数据
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
函数来间接达成模糊查询的作用
条件更新数据
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表数据的变更
条件删除数据
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(一对多查询)
以User
和Order
举例,一个用户可以有多个订单的场景下我们应该如何定义模型
// 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"},
}
}
// 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
,表示一个订单只属于一个用户
当我们想要进行联表查询时就可以进行如下查询了:
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(一对一查询)
以User
和Profile
举例,一个用户只拥有一份个人资料的情况下应该如何定义模型
// 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"},
}
}
// 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
可以有多个order
用edge.From
表示一个order
只归属于一个user
。为什么到了O2O的列子中,依旧需要用到To
和From
来定义关系?而到底哪个才应该是To
来表示哪个应该用From
表示?原因在于在ent框架中,
edge.To
和edge.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.To
和edge.From
来定义边缘的方向。例如,在User
和Profile
的例子中,我们在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.To
和edge.From
的使用取决于你想要如何定义数据关系的指向,而不仅仅是关系的类型(一对多或一对一)
O2O的联表查询和O2M的查询方式基本一致,这里就不再赘述。
M2M(多对多查询)
以User
和Group
举例,一个用户可以存在多个用户组中,一个用户组也可以包含多个用户。这里根据实际场景我们有两种处理方式
方案一 我们需要自定义UserGroup中间表
// 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"},
}
}
// 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"},
}
}
// 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为我们生成中间表
// 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"},
}
}
// 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的方式类似,这里也不再赘述,大家可以下来自己尝试一