搞懂常见Go ORM系列-Ent框架详解

一、Ent简介

Ent 是一个用于 Go 语言的 ORM(对象关系映射)框架

它的最大特点是通过代码生成的方式,提供类型安全的数据库访问能力

在使用 Ent 时,首先需要定义 schema,然后通过工具生成数据库访问代码,这样不仅提高了开发效率,还能避免常见的运行时错误

Ent 支持多种关系型数据库,包括 MySQL、PostgreSQL、SQLite 和 Microsoft SQL Server,并且对数据库结构和关系的支持非常全面

二、Ent的基本用法

1. 定义Schema

在使用 Ent 时,开发者需要先定义 schema,这是 Ent 中模型的基础。一个 schema 定义了数据库表的结构,以及与其他表之间的关系。

Ent 使用 Go 语言结构体来定义 schema,可以使用命令快速创建一个或者多个schema。例如,创建用户(User)和文章(Post)模型

 

shell

代码解读

复制代码

go run -mod=mod entgo.io/ent/cmd/ent new User Post # 命令会自动创建如下文件夹或者文件 ent ├── generate.go └── schema ├── post.go └── user.go

通过Fields()定义了用户(User)和文章(Post)模型的字段

通过 Edges 定义了它们之间的关系: User 有多个 Post,而每个 Post 又有一个关联的 User

 

go

代码解读

复制代码

// ent/schema/user.go package schema import ( "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" ) // User represents a user in the database. type User struct { ent.Schema } // Fields of the User. func (User) Fields() []ent.Field { return []ent.Field{ field.Int("age").Default(0), field.String("name").NotEmpty(), field.String("email").Unique(), } } // Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("posts", Post.Type), } }

 

go

代码解读

复制代码

// ent/schema/post.go package schema import ( "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" ) // Post represents a post written by a user. type Post struct { ent.Schema } // Fields of the Post. func (Post) Fields() []ent.Field { return []ent.Field{ field.String("title").NotEmpty(), field.String("content").NotEmpty(), } } // Edges of the Post. func (Post) Edges() []ent.Edge { return []ent.Edge{ edge.From("author", User.Type). Ref("posts"). Unique(), } }

2. 生成代码

在使用 Ent 前,需要先生成代码。生成过程会根据我们定义的 schema 自动生成模型类、查询构建器和数据库操作方法。使用以下命令:

 

bash

代码解读

复制代码

go generate ./ent

这个命令会根据我们定义的 User 和 Post schema 生成对应的代码文件,并提供查询、插入、更新等操作的方法。

 

go

代码解读

复制代码

ent ├── client.go ├── ent.go ├── enttest ├── generate.go ├── hook ├── migrate ├── mutation.go ├── post ├── post.go ├── post_create.go ├── post_delete.go ├── post_query.go ├── post_update.go ├── predicate ├── runtime ├── runtime.go ├── schema ├── tx.go ├── user ├── user.go ├── user_create.go ├── user_delete.go ├── user_query.go └── user_update.go

3. 创建Client

在开始使用 Ent 进行数据库操作之前,我们首先需要创建一个数据库的 client

以 SQLite 为例,创建一个 client 可以通过以下代码完成:

 

go

代码解读

复制代码

package main import ( "context" "log" "entdemo/ent" _ "github.com/mattn/go-sqlite3" ) func main() { client, err := ent.Open("sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") if err != nil { log.Fatalf("failed opening connection to sqlite: %v", err) } defer client.Close() ctx := context.Background() // Run the auto migration tool. if err := client.Schema.Create(ctx); err != nil { log.Fatalf("failed creating schema resources: %v", err) } // ... }

在这段代码中,我们通过 ent.Open 创建了 client 时指定了使用的数据库驱动和连接信息,并使用client.Schema.Create(ctx)自动创建了表结构

4. 执行数据库操作

创建 client 后,我们就可以通过它来执行数据库操作

这段代码演示了先创建用户,并创建了一篇文章作者为此用户

 

go

代码解读

复制代码

{ // ... user, err := client.User.Create(). SetName("Alice"). SetEmail("alice@example.com"). Save(ctx) if err != nil { log.Fatal("failed to create user:", err) } post, err := client.Post.Create(). SetTitle("Hello"). SetContent("World!"). SetAuthor(user). Save(ctx) if err != nil { log.Fatal("failed to create post:", err) } log.Println("post was created: ", post) }

5. 小结

可以看出来ent通过code gen来满足了类型安全,同时生成出来的相对友好的函数api。下面来继续看看ent提供的其他强大的支持

四、常规CURD

所有的CURD操作都有对应的X函数,例如SaveXFirstX,代表不返回错误,如果发生错误直接panic

1. 创建记录

单条创建

创建一个新的用户并保存到数据库:

 

go

代码解读

复制代码

user, err := client.User.Create(). SetName("Alice"). SetEmail("alice@example.com"). Save(ctx) if err != nil { log.Fatal("failed to create user:", err) } // SaveX 当发生错误时会panic user := client.User.Create(). SetName("Alice"). SetEmail("alice@example.com"). SaveX(ctx) if err != nil { log.Fatal("failed to create user:", err) }

批量创建
 

go

代码解读

复制代码

users, err := client.User.CreateBulk( client.User.Create().SetName("Alice").SetEmail("alice@example.com"), client.User.Create().SetName("Bob").SetEmail("bob@example.com"), ).Save(ctx) if err != nil { log.Fatal("failed to create user:", err) } log.Println("users was created: ", users)

基于已有的列表批量创建
 

go

代码解读

复制代码

names := []string{"pedro", "xabi", "layla"} users, err := client.User.MapCreateBulk(names, func(c *ent.UserCreate, i int) { c.SetName(names[i]).SetEmail(fmt.Sprintf("%s@example.com", names[i])) }).Save(ctx) log.Println("users was created: ", users)

2. 查询记录

批量查询

可以看到Ent通过schema生成出了各种查询条件,以及排序。甚至可以使用关联表

这些查询条件或者排序在后面所示的所有查询中都可以使用

 

go

代码解读

复制代码

users, err := client.User.Query(). Where( user.HasPosts(), user.Or( user.NameEQ("Bob"), ), user.Not( user.EmailEQ("alice2@example.com"), ), ). Order( user.ByPostsCount(sql.OrderDesc()), ). Limit(2). All(ctx) log.Println("users: ", users)

单条查询

查找第一条,如果不存在返回*NotFoundError

 

go

代码解读

复制代码

user, err := client.User.Query(). Where( user.NameEQ("Alice"), ). First(ctx) if err != nil { log.Fatal("failed to query user:", err) }

查找唯一一条,如果不存在返回*NotFoundError,如果存在一条以上返回*NotSingularError

 

go

代码解读

复制代码

user, err := client.User.Query(). Where( user.NameEQ("Alice"), ). Only(ctx) if err != nil { log.Fatal("failed to query user:", err) }

指定查询字段

可以使用Select指定返回的字段

1、仅查询模型id,而不是实体

IDs返回对应的id切片

FirstID返回第一条记录的id,如果不存在返回*NotFoundError

OnlyID返回唯一一条记录的id,如果不存在返回*NotFoundError,如果存在一条以上返回*NotSingularError

 

go

代码解读

复制代码

ids, err := client.User.Query(). Where(user.NameEQ("Alice")). IDs(ctx) if err != nil { log.Fatal("failed to query ids:", err) } log.Println("ids: ", ids) // ids: [1 2]

2、使用All时,返回的模型仅填充选择的字段

 

go

代码解读

复制代码

names, err := client.User.Query(). Select(user.FieldName). All(ctx) if err != nil { log.Fatal("failed to query names:", err) } log.Println("names: ", names) // names: [User(id=1, name=Alice, email=) User(id=2, name=Alice, email=) User(id=3, name=Bob, email=)]

3、可以指定具体类型

StringsFloat64sInts等返回对应类型的切片

StringFloat64Int等返回单条值,如果有多条则报错

 

go

代码解读

复制代码

names, err := client.User.Query(). Select(user.FieldName). Strings(ctx) if err != nil { log.Fatal("failed to query names:", err) } log.Println("names: ", names) // names: [Alice Alice Bob]

4、也可以使用自定义类型

其中结构体的字段必须覆盖所有Select的字段

 

go

代码解读

复制代码

var v []struct { Email string `json:"email"` Name string `json:"name"` } err := client.User.Query(). Select(user.FieldName, user.FieldEmail). Scan(ctx, &v) if err != nil { log.Fatal("failed to query names:", err) } log.Println("users: ", v) // users: [{alice@example.com Alice} {alice2@example.com Alice} {bob@example.com Bob}]

3. 更新记录

模型更新

无论是创建还是查询出来的模型,都可以调用.Update()来更新

 

go

代码解读

复制代码

user, err := client.User.Create(). SetName("Alice"). SetEmail("alice@example.com"). Save(ctx) if err != nil { log.Fatal("failed to create user:", err) } log.Println("user was created: ", user) { user, err = user.Update(). SetName("Alice2"). AddAge(10). Save(ctx) if err != nil { log.Fatal("failed to create user:", err) } log.Println("user was updated: ", user) }

也可以这样使用模型来更新,效果同上

 

go

代码解读

复制代码

{ user, err = client.User.UpdateOne(user). SetName("Alice2"). AddAge(10). Save(ctx) if err != nil { log.Fatal("failed to create user:", err) } log.Println("user was updated: ", user) }

按ID更新

如果id不存在会报错*NotFoundError

 

go

代码解读

复制代码

user, err = client.User.UpdateOneID(1). SetName("Alice2"). AddAge(10). Save(ctx) if err != nil { log.Fatal("failed to create user:", err) } log.Println("user was updated: ", user)

批量更新

更新操作会返回修改了多少行

 

go

代码解读

复制代码

n, err := client.User.Update(). SetName("Alice2"). AddAge(10). Where( user.EmailEQ("alice@example.com"), ). Save(ctx) if err != nil { log.Fatal("failed to create user:", err) } log.Println("users was updated: ", n)

4. 删除记录

删除模型
 

go

代码解读

复制代码

u, err := client.User.Query(). Where(user.Name("Alice")). First(ctx) if err != nil { log.Fatal("failed to query user:", err) } err = client.User.DeleteOne(u). Exec(ctx) if err != nil { log.Fatal("failed to delete user:", err) }

按ID删除

如果id不存在会报错*NotFoundError

 

go

代码解读

复制代码

err = client.User.DeleteOneID(1). Exec(ctx) if err != nil { log.Fatal("failed to delete user:", err) }

批量删除

删除操作会返回删除了多少条记录

 

go

代码解读

复制代码

num, err := client.User.Delete(). Where(user.NameEQ("Alice")). Exec(ctx) if err != nil { log.Fatal("failed to delete user:", err) } log.Println("delete: ", num)

五、定义Schema的Fields类型

在 Ent 中,Fields 定义了模型的字段和其数据类型

通过 Fields,开发者可以精确控制数据库表中每个字段的类型、约束和默认值。

Ent 提供了多种字段类型和配置选项,方便开发者进行自定义。

1. 字段类型

Ent 支持多种常用的字段类型,包括字符串、整型、浮动点、布尔值、枚举等。以下是一些常见字段类型的示例:

 

go

代码解读

复制代码

package schema import ( "time" "entgo.io/ent" "entgo.io/ent/schema/field" ) // Event schema represents an event entity. type Event struct { ent.Schema } // Fields defines the fields for the Event schema. func (Event) Fields() []ent.Field { return []ent.Field{ field.String("name").NotEmpty(), // 字符串类型字段,不能为空 field.String("email").Unique(), // 唯一索引 field.Int("age").Positive(), // 整型字段,值必须大于零 field.Float("balance").Default(0.0), // 浮动类型字段,默认值为0.0 field.Bool("active").Default(true), // 布尔类型字段,默认值为true field.Enum("size").Values("big", "small"), // 枚举类型,可选值:big|small field.Time("start_time"). // 时间类型 Default(time.Now), // 默认值为当前时间 } }

2. 字段约束

Ent 提供了多种约束和验证方式,可以通过链式调用来对字段进行配置。

常见的字段约束如下:

  • NotEmpty():要求字段不能为空。
  • Unique():确保字段值唯一。
  • Positive():限制整型字段的值必须大于零。
  • Default(value):为字段设置默认值。
  • Min(value)Max(value):设置数值类型字段的最小值或最大值。

六、高级用法

1. 事务的使用

Ent 支持事务,保证在多个数据库操作中所有操作要么全部成功,要么全部失败

例如,在创建用户和文章时,我们可以通过事务来确保这两个操作要么都成功,要么都失败:

我们首先开启一个事务(client.Tx()),然后在事务中执行用户和文章的创建操作

如果有任何一个操作失败,我们会回滚事务。只有在所有操作都成功时,我们才会提交事务

 

go

代码解读

复制代码

// 开始一个事务 tx, err := client.Tx(ctx) if err != nil { log.Fatal("failed to begin a transaction:", err) } // 在事务中执行操作 user, err := tx.User.Create(). SetName("Alice"). SetEmail("alice@example.com"). Save(ctx) if err != nil { tx.Rollback() // 操作失败时回滚事务 log.Fatal("failed to create user:", err) } _, err = tx.Post.Create(). SetTitle("Hello World"). SetContent("This is my first post"). SetAuthor(user). Save(context.Background()) if err != nil { tx.Rollback() // 操作失败时回滚事务 log.Fatal("failed to create post:", err) } // 提交事务 if err := tx.Commit(); err != nil { log.Fatal("failed to commit transaction:", err) } log.Println("transaction committed")

2. 支持的数据库关系

Ent 使用edge.To 和 edge.From创建数据关系,包括一对一、一对多、多对多等。

以下是一些常见的关系类型的示例:

假设每个用户有一个对应的档案(Profile),这是一对一的关系

每个用户可以有多个帖子(Post),这是一个典型的一对多关系

每个用户可以加入多个小组(Group),每个小组也可以有多个成员,这是一个多对多关系

我们可以通过以下代码定义

 

go

代码解读

复制代码

// Edges of the User. func (User) Edges() []ent.Edge { return []ent.Edge{ edge.To("profile", Profile.Type).Unique(), // 一对一,每个用户有一个对应的档案 edge.To("posts", Post.Type), //一对多,每个用户可以有多个帖子(Post) edge.To("groups", Group.Type), // 多对多,每个用户可以加入多个小组(Group),每个小组也可以有多个成员 } } func (Profile) Edges() []ent.Edge { return []ent.Edge{ // 默认会在profile表增加列user_profile,可以使用Field()自定义列名 // From的第一个参数不影响列名,只决定code gen出来的函数名QueryUser() edge.From("user", User.Type). Ref("profile"). Unique(), // 一对一,每个用户有一个对应的档案 } } func (Post) Edges() []ent.Edge { return []ent.Edge{ // 默认会在profile表增加列user_posts,可以使用Field()自定义列名 // From的第一个参数不影响列名,只决定code gen出来的函数名QueryAuthor() edge.From("author", User.Type). Ref("posts"). Unique(), //一对多,每个用户可以有多个帖子(Post) } } func (Group) Edges() []ent.Edge { return []ent.Edge{ // 默认会创建中间表user_groups edge.From("members", User.Type). Ref("groups"), // 多对多,每个用户可以加入多个小组(Group),每个小组也可以有多个成员 } }

我们还可以Atlas这个工具将关系可视化

安装Atlas

 

shell

代码解读

复制代码

curl -sSf https://atlasgo.sh | sh

然后执行

 

shell

代码解读

复制代码

atlas schema inspect \ -u "ent://ent/schema" \ --dev-url "sqlite://file?mode=memory&_fk=1" \ -w

就可以得到

Atlas展示关系

七、总结

最后我们来回顾下Ent的使用方式

  • 定义 schema
  • 生成代码
  • 创建Client
  • 执行数据库操作

Ent通过代码生成的方式,不仅提高了开发效率,还能避免常见的运行时错误

Ent的功能还有很多,更多细枝末节的概念大家可以查询Ent官方文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值