DEV Community

Cover image for 封装 Sqlc 的 Queries 实现更方便的事务操作
鸿则
鸿则

Posted on

封装 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)
}
Enter fullscreen mode Exit fullscreen mode

对事务的封装

下面这段代码是一个巧妙的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)
}
Enter fullscreen mode Exit fullscreen mode

函数首先开始一个新的事务,然后通过延迟执行来确保事务最终会被回滚,除非它被明确提交。这是一个安全机制,防止未完成的事务占用资源。接着,函数调用用户提供的回调,传入一个带有事务上下文的查询对象,允许用户在事务中执行所需的数据库操作。

如果回调成功执行且没有错误,函数会提交事务。任何在过程中出现的错误都会导致事务回滚。这种方法既保证了数据一致性,又大大简化了错误处理。

这个封装的优雅之处在于,它将复杂的事务管理逻辑隐藏在一个简单的函数调用之后。用户可以专注于编写业务逻辑,而不必担心事务的开始、提交或回滚。

这段代码的使用方法相当直观。你可以在需要执行事务的地方调用 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")
}
Enter fullscreen mode Exit fullscreen mode

在这个例子中,我们在事务中创建了一个用户和一个帖子。如果任何操作失败,整个事务都会回滚。如果所有操作都成功,事务会被提交。

这种方法的好处是你不需要手动管理事务的开始、提交或回滚,所有这些都由 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),
    }
}
Enter fullscreen mode Exit fullscreen mode

但是当我们开始编写代码的时候就会发现,*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),
    }
}
Enter fullscreen mode Exit fullscreen mode

现在,我们的 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)
}
Enter fullscreen mode Exit fullscreen mode

这个方法和上一章节实现的 WithTransaction 主要不同是,他是实现在 *Repositories 上面而不是全局的,这样我们就可以通过 (r *Repositories) 中的 pgx.Tx 来开始嵌套事务了。

在没有开始事务的时候,我们可以调用 repositories.WithTransaction 来开启一个新的事务。

err := db.repositories.WithTransaction(ctx, func(tx *db.Repositories) error {

    return nil
})
Enter fullscreen mode Exit fullscreen mode

多级事务也是没有问题的,非常容易实现。

err := db.repositories.WithTransaction(ctx, func(tx *db.Repositories) error {
    // 假设此处进行了一些数据操作
    // 然后,开启一个嵌套事务
    return tx.WithTransaction(ctx, func(tx *db.Repositories) error {
        // 这里可以在嵌套事务中进行一些操作
        return nil
    })
})
Enter fullscreen mode Exit fullscreen mode

这个封装方案有效地确保了操作的原子性,即使其中任何一个操作失败,整个事务也会被回滚,从而保障了数据的一致性。

结束语

本文介绍了一个使用 Go 和 pgx 库封装 SQLC 数据库事务的方案。

核心是 Repositories 结构体,它封装了 SQLC 查询接口和事务处理逻辑。通过 WithTransaction 方法,我们可以在现有事务上开始新的子事务或在连接池中开始新的事务,并确保在函数返回时回滚事务。

构造函数 NewRepositories 和 NewRepositoriesTx 分别用于创建普通和事务内的 Repositories 实例。

这样可以将多个数据库操作封装在一个事务中,如果任何一个操作失败,事务将被回滚,提高了代码的可维护性和可读性。

Top comments (1)

Collapse
 
benebobaa profile image
Benediktus Satriya

why its fully with 函数首先开始一个新的事务,然后通过 words