在Go ORM开篇中我们将Go ORM框架分成了三类
🌲 反射型 主要通过反射机制将结构体映射到数据库表上,代表作为 go-gorm/gorm
🌲 代码生成型 通过代码生成工具预先生成数据模型及查询构建器,代表作有 ent/ent 和日益流行的 go-gorm/gen
🌲 SQL 增强型 基于原生 SQL 库进行封装和扩展,既保留 SQL 的灵活性,又提供了一系列便捷函数,代表作为 jmoiron/sqlx
如果还没有读过总览,请阅读: 搞懂常见Go ORM系列-开篇
Ent
作为代码生成型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("[email protected]").
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("[email protected]").
Save(ctx)
if err != nil {
log.Fatal("failed to create user:", err)
}
// SaveX 当发生错误时会panic
user := client.User.Create().
SetName("Alice").
SetEmail("[email protected]").
SaveX(ctx)
if err != nil {
log.Fatal("failed to create user:", err)
}
批量创建
go
users, err := client.User.CreateBulk(
client.User.Create().SetName("Alice").SetEmail("[email protected]"),
client.User.Create().SetName("Bob").SetEmail("[email protected]"),
).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("%[email protected]", 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("[email protected]"),
),
).
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: [{[email protected] Alice} {[email protected] Alice} {[email protected] Bob}]
3. 更新记录
模型更新
无论是创建还是查询出来的模型,都可以调用.Update()
来更新
go
user, err := client.User.Create().
SetName("Alice").
SetEmail("[email protected]").
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("[email protected]"),
).
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("[email protected]").
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
官方文档
✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的知识 ✨