SQLC は、SQL クエリをタイプセーフな Go コードに変換することを主な機能とする強力な開発ツールです。 SQL ステートメントを解析し、データベース構造を分析することにより、sqlc は対応する Go 構造と関数を自動的に生成し、データベース操作のコード作成プロセスを大幅に簡素化します。
sqlc を使用すると、開発者は SQL クエリの作成に集中し、面倒な Go コード生成作業をツールに任せることができるため、開発プロセスが加速され、コードの品質が向上します。
Sqlc によって生成されたコードには通常、すべてのデータベース操作をカプセル化するクエリ構造が含まれています。この構造は、すべてのデータベース クエリ メソッドを定義する一般的な Querier インターフェイスを実装します。
重要なのは、sqlc によって生成された New 関数は、*sql.DB や *sql.Tx などの DBTX インターフェイスを実装する任意のオブジェクトを受け入れることができるということです。
トランザクション実装の核心は、Go のインターフェース多態性を利用することです。トランザクション内で操作を実行する必要がある場合は、*sql.Tx オブジェクトを作成し、それを New 関数に渡して新しい Queries インスタンスを作成します。このインスタンスは、トランザクションのコンテキスト内ですべての操作を実行します。
pgx を介して Postgres データベースに接続し、次のコードでクエリを初期化するとします。
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) }
次のコードは、Go でデータベース トランザクションを使用するプロセスを簡素化する、賢い SQL トランザクションのカプセル化です。この関数は、コンテキストとコールバック関数をパラメータとして受け取ります。このコールバック関数は、ユーザーがトランザクションで実行したい特定の操作です。
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 を拡張する構造は、*sqlc.Queries を拡張し、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 には、トランザクションを開始するための Begin メソッドしか含まれていないためです。この問題を解決するために、引き続きリポジトリを拡張し、それに新しい属性 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), } }
リポジトリ構造には pool 属性と tx 属性の両方が存在しますが、これはあまり洗練されていないように見えるかもしれません。実際、これが上記の理由、つまり *pgxpool です。 .Pool トランザクションを開始するメソッドのみがあり、トランザクションを終了するメソッドはありません。この問題を解決する 1 つの方法は、*pgxpool.Pool の代わりに別の RepositoriesTX 構造を作成し、その中に pgx.Tx を格納することです。新しい質問の 1 つは、両方に WithTransaction メソッドを実装する必要があるということです。もう 1 つの質問については、後で説明します。まず、リポジトリの 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 をカプセル化してより便利なトランザクション操作を実装するクエリの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。