SQLC 是一個強大的開發工具,它的核心功能是將SQL查詢轉換成型別安全的Go程式碼。透過解析SQL語句和分析資料庫結構,sqlc能夠自動產生對應的Go結構體和函數,大大簡化了資料庫操作的程式碼編寫過程。
使用sqlc,開發者可以專注於編寫SQL查詢,而將繁瑣的Go程式碼產生工作交給工具完成,從而加速開發過程並提高程式碼品質。
Sqlc 產生的程式碼通常包含一個Queries結構體,它封裝了所有資料庫操作。這個結構體實作了一個通用的Querier接口,該接口定義了所有資料庫查詢方法。
關鍵在於,sqlc產生的New函數可以接受任何實作了DBTX介面的對象,包括*sql.DB和*sql.Tx。
事務實現的核心在於利用Go的介面多態性。當你需要在事務中執行操作時,可以建立一個*sql.Tx對象,然後將其傳遞給New函數來建立一個新的Queries實例。這個實例會在交易的上下文中執行所有操作。
假設我們透過 pgx 連接 Postgres 資料庫,並以下程式碼初始化 Queries。
var Pool *pgxpool.Pool var Queries *sqlc.Queries func init() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() connConfig, err := pgxpool.ParseConfig("postgres://user:password@127.0.0.1:5432/db?sslmode=disable") if err != nil { panic(err) } pool, err := pgxpool.NewWithConfig(ctx, connConfig) if err != nil { panic(err) } if err := pool.Ping(ctx); err != nil { panic(err) } Pool = pool Queries = sqlc.New(pool) }
下面這段程式碼是個巧妙的sqlc交易封裝,它簡化了在Go中使用資料庫事務的過程。函數接受一個上下文和一個回呼函數作為參數,這個回呼函數就是使用者想在交易中執行的具體操作。
func WithTransaction(ctx context.Context, callback func(qtx *sqlc.Queries) (err error)) (err error) { tx, err := Pool.Begin(ctx) if err != nil { return err } defer func() { if e := tx.Rollback(ctx); e != nil && !errors.Is(e, pgx.ErrTxClosed) { err = e } }() if err := callback(Queries.WithTx(tx)); err != nil { return err } return tx.Commit(ctx) }
函數首先開始一個新的事務,然後透過延遲執行來確保事務最終會被回滾,除非它被明確提交。這是一個安全機制,防止未完成的事務佔用資源。接著,函數呼叫使用者提供的回調,傳入一個帶有事務上下文的查詢對象,允許使用者在事務中執行所需的資料庫操作。
如果回呼成功執行且沒有錯誤,函數會提交交易。任何在過程中出現的錯誤都會導致交易回滾。這種方法既保證了資料一致性,也大大簡化了錯誤處理。
這個封裝的優雅之處在於,它將複雜的事務管理邏輯隱藏在一個簡單的函式呼叫之後。使用者可以專注於編寫業務邏輯,而不必擔心交易的開始、提交或回滾。
這段程式碼的使用方法相當直觀。你可以在需要執行事務的地方呼叫 db.WithTransaction 函數,並傳入一個函數作為參數,該函數定義了你想在事務中執行的所有資料庫操作。
err := db.WithTransaction(ctx, func(qtx *sqlc.Queries) error { // 在这里执行你的数据库操作 // 例如: _, err := qtx.CreateUser(ctx, sqlc.CreateUserParams{ Name: "Alice", Email: "alice@example.com", }) if err != nil { return err } _, err = qtx.CreatePost(ctx, sqlc.CreatePostParams{ Title: "First Post", Content: "Hello, World!", AuthorID: newUserID, }) if err != nil { return err } // 如果所有操作都成功,返回 nil return nil }) if err != nil { // 处理错误 log.Printf("transaction failed: %v", err) } else { log.Println("transaction completed successfully") }
在這個例子中,我們在事務中建立了一個使用者和一個貼文。如果任何操作失敗,整個交易都會回滾。如果所有操作都成功,事務會被提交。
這種方法的好處是你不需要手動管理事務的開始、提交或回滾,所有這些都由 db.WithTransaction 函數處理。你只需要專注於在事務中執行的實際資料庫操作。這大大簡化了程式碼,並減少了出錯的可能性。
上述的這種封裝方式並非毫無缺點。
這種簡單的事務封裝在處理巢狀事務時有其限制。這是因為它每次都會創建一個新的事務,而不是檢查是否已經在一個事務中。
為了實現巢狀事務處理,我們必須可以獲得當前事務對象,但是當前事務對像是隱藏在 sqlc.Queries 內部的,所以必須我們需要擴展 sqlc.Queries。
擴展 sqlc.Queries 的結構體被我們創建為 Repositories,他擴展了 *sqlc.Queries 並添加了一個新的屬性 pool,這是一個 pgxpool.Pool 類型的指標。
type Repositories struct { *sqlc.Queries pool *pgxpool.Pool } func NewRepositories(pool *pgxpool.Pool) *Repositories { return &Repositories{ pool: pool, Queries: sqlc.New(pool), } }
但是當我們開始編寫程式碼的時候就會發現,*pgxpool.Pool 並不能滿足pgx.Tx 接口,這是因為*pgxpool.Pool 中缺少Rollback 和Commit 方法,他只包含用於開始事務的Begin方法,為了解決這個問題,我們繼續擴展Repositories 在其中新增一個新的屬性tx,並為其添加新的NewRepositoriesTx 方法。
type Repositories struct { *sqlc.Queries tx pgx.Tx pool *pgxpool.Pool } func NewRepositoriesTx(tx pgx.Tx) *Repositories { return &Repositories{ tx: tx, Queries: sqlc.New(tx), } }
現在,我們的Repositories 結構體中同時存在pool 和tx 屬性,這可能看起來不是很優雅,為什麼不能抽像出來一個統一的TX 類型呢,其實還是上面說到的原因,即*pgxpool.Pool只有開始事務的方法,而沒有結束事務的方法,而解決這個問題的方法之一是,再創建一個RepositoriesTX 結構體,在其中存儲pgx.Tx 而不是*pgxpool.Pool ,但是這樣做可能又會帶來新的問題,其中之一是,我們可能要為他們兩者分別實作WithTransaction 方法,至於另外一個問題,我們後面在說,現在讓我們先來實作Repositories 的WithTransaction 方法。
func (r *Repositories) WithTransaction(ctx context.Context, fn func(qtx *Repositories) (err error)) (err error) { var tx pgx.Tx if r.tx != nil { tx, err = r.tx.Begin(ctx) } else { tx, err = r.pool.Begin(ctx) } if err != nil { return err } defer func() { if e := tx.Rollback(ctx); e != nil && !errors.Is(e, pgx.ErrTxClosed) { err = e } }() if err := fn(NewRepositoriesTx(tx)); err != nil { return err } return tx.Commit(ctx) }
这个方法和上一章节实现的 WithTransaction 主要不同是,他是实现在 *Repositories 上面而不是全局的,这样我们就可以通过 (r *Repositories) 中的 pgx.Tx 来开始嵌套事务了。
在没有开始事务的时候,我们可以调用 repositories.WithTransaction 来开启一个新的事务。
err := db.repositories.WithTransaction(ctx, func(tx *db.Repositories) error { return nil })
多级事务也是没有问题的,非常容易实现。
err := db.repositories.WithTransaction(ctx, func(tx *db.Repositories) error { // 假设此处进行了一些数据操作 // 然后,开启一个嵌套事务 return tx.WithTransaction(ctx, func(tx *db.Repositories) error { // 这里可以在嵌套事务中进行一些操作 return nil }) })
这个封装方案有效地确保了操作的原子性,即使其中任何一个操作失败,整个事务也会被回滚,从而保障了数据的一致性。
本文介绍了一个使用 Go 和 pgx 库封装 SQLC 数据库事务的方案。
核心是 Repositories 结构体,它封装了 SQLC 查询接口和事务处理逻辑。通过 WithTransaction 方法,我们可以在现有事务上开始新的子事务或在连接池中开始新的事务,并确保在函数返回时回滚事务。
构造函数 NewRepositories 和 NewRepositoriesTx 分别用于创建普通和事务内的 Repositories 实例。
这样可以将多个数据库操作封装在一个事务中,如果任何一个操作失败,事务将被回滚,提高了代码的可维护性和可读性。
以上是封裝 Sqlc 的 Queries 實作更方便的事務操作的詳細內容。更多資訊請關注PHP中文網其他相關文章!