搞懂常見 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 函數,例如SaveX
、FirstX
,代表不返回錯誤,如果發生錯誤直接 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、可以指定具體類型
Strings
、Float64s
、Ints
等返回對應類型的切片
String
、Float64
、Int
等返回單條值,如果有多條則報錯
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 提供了多種約束和驗證方式,可以通過鏈式調用來對字段進行配置。
常見的字段約束如下:
-
NotEmpty()
:要求字段不能爲空。 -
Unique()
:確保字段值唯一。 -
Positive()
:限制整型字段的值必須大於零。 -
Default(value)
:爲字段設置默認值。 -
Min(value)
、Max(value)
:設置數值類型字段的最小值或最大值。
六、高級用法
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 的使用方式
-
定義 schema
-
生成代碼
-
創建 Client
-
執行數據庫操作
Ent
通過代碼生成的方式,不僅提高了開發效率,還能避免常見的運行時錯誤
Ent
的功能還有很多,更多細枝末節的概念大家可以查詢Ent
官方文檔
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/57Y6IxUb9ZWBhJ0rfjsZPA