首頁  >  文章  >  後端開發  >  封裝 Sqlc 的 Queries 實作更方便的事務操作

封裝 Sqlc 的 Queries 實作更方便的事務操作

王林
王林原創
2024-08-05 18:39:20784瀏覽

封装 Sqlc 的 Queries 实现更方便的事务操作

SQLC 是什麼

SQLC 是一個強大的開發工具,它的核心功能是將SQL查詢轉換成型別安全的Go程式碼。透過解析SQL語句和分析資料庫結構,sqlc能夠自動產生對應的Go結構體和函數,大大簡化了資料庫操作的程式碼編寫過程。

使用sqlc,開發者可以專注於編寫SQL查詢,而將繁瑣的Go程式碼產生工作交給工具完成,從而加速開發過程並提高程式碼品質。

SQLC 的交易實現

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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn