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

在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

这个命令会根据我们定义的 UserPost 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函数,例如SaveXFirstX,代表不返回错误,如果发生错误直接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、可以指定具体类型

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: [{[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.Toedge.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官方文档


✨ 微信公众号【凉凉的知识库】同步更新,欢迎关注获取最新最有用的知识 ✨

相关推荐
IT成长日记3 小时前
【MySQL基础】聚合函数从基础使用到高级分组过滤
数据库·mysql·聚合函数
Guarding and trust4 小时前
python系统之综合案例:用python打造智能诗词生成助手
服务器·数据库·python
夜间出没的AGUI5 小时前
SQLiteBrowser 的详细说明,内容结构清晰,涵盖核心功能、使用场景及实用技巧
数据库
不再幻想,脚踏实地6 小时前
MySQL(一)
java·数据库·mysql
Tyler先森7 小时前
Oracle数据库数据编程SQL<3.5 PL/SQL 存储过程(Procedure)>
数据库·sql·oracle
KevinRay_7 小时前
从零开始学习SQL
数据库·学习·mysql
Json_181790144808 小时前
python采集淘宝拍立淘按图搜索API接口,json数据示例参考
服务器·前端·数据库
Albert Tan8 小时前
Oracle 10G DG 修复从库-磁盘空间爆满导致从库无法工作
数据库·oracle
好记忆不如烂笔头abc8 小时前
oracle-blob导出,在ob导入失败
大数据·数据库·python