搞懂常見 Go ORM 系列 - Ent 框架詳解

在 Go ORM 開篇中我們將 Go ORM 框架分成了三類

🌲 反射型主要通過反射機制將結構體映射到數據庫表上,代表作爲 go-gorm/gorm

🌲 代碼生成型通過代碼生成工具預先生成數據模型及查詢構建器,代表作有 ent/ent 和日益流行的 go-gorm/gen

🌲 SQL 增強型基於原生 SQL 庫進行封裝和擴展,既保留 SQL 的靈活性,又提供了一系列便捷函數,代表作爲 jmoiron/sqlx

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)模型

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

// 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),
    }
}
// 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 自動生成模型類、查詢構建器和數據庫操作方法。使用以下命令:

go generate ./ent

這個命令會根據我們定義的 User 和 Post schema 生成對應的代碼文件,並提供查詢、插入、更新等操作的方法。

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 可以通過以下代碼完成:

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 後,我們就可以通過它來執行數據庫操作

這段代碼演示了先創建用戶,並創建了一篇文章作者爲此用戶

{
   // ...
   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. 創建記錄

單條創建

創建一個新的用戶並保存到數據庫:

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)
}

批量創建

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)

基於已有的列表批量創建

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 生成出了各種查詢條件,以及排序。甚至可以使用關聯表

這些查詢條件或者排序在後面所示的所有查詢中都可以使用

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

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

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

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

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時,返回的模型僅填充選擇的字段

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等返回單條值,如果有多條則報錯

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 的字段

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()來更新

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)
}

也可以這樣使用模型來更新,效果同上

{
    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

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)

批量更新

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

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. 刪除記錄

刪除模型

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

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

批量刪除

刪除操作會返回刪除了多少條記錄

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 支持多種常用的字段類型,包括字符串、整型、浮動點、布爾值、枚舉等。以下是一些常見字段類型的示例:

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 提供了多種約束和驗證方式,可以通過鏈式調用來對字段進行配置。

常見的字段約束如下:

六、高級用法

1. 事務的使用

Ent 支持事務,保證在多個數據庫操作中所有操作要麼全部成功,要麼全部失敗

例如,在創建用戶和文章時,我們可以通過事務來確保這兩個操作要麼都成功,要麼都失敗:

我們首先開啓一個事務(client.Tx()),然後在事務中執行用戶和文章的創建操作

如果有任何一個操作失敗,我們會回滾事務。只有在所有操作都成功時,我們纔會提交事務

// 開始一個事務
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),每個小組也可以有多個成員,這是一個多對多關係

我們可以通過以下代碼定義

// 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

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

然後執行

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

就可以得到

七、總結

最後我們來回顧下 Ent 的使用方式

Ent通過代碼生成的方式,不僅提高了開發效率,還能避免常見的運行時錯誤

Ent的功能還有很多,更多細枝末節的概念大家可以查詢Ent官方文檔


本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/57Y6IxUb9ZWBhJ0rfjsZPA