一、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函数,例如SaveX
、FirstX
,代表不返回错误,如果发生错误直接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、可以指定具体类型
Strings
、Float64s
、Ints
等返回对应类型的切片
String
、Float64
、Int
等返回单条值,如果有多条则报错
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
就可以得到
七、总结
最后我们来回顾下Ent的使用方式
- 定义 schema
- 生成代码
- 创建Client
- 执行数据库操作
Ent
通过代码生成的方式,不仅提高了开发效率,还能避免常见的运行时错误
Ent
的功能还有很多,更多细枝末节的概念大家可以查询Ent
官方文档