Maison  >  Article  >  développement back-end  >  Requêtes encapsulant Sqlc pour implémenter des opérations de transaction plus pratiques

Requêtes encapsulant Sqlc pour implémenter des opérations de transaction plus pratiques

王林
王林original
2024-08-05 18:39:20656parcourir

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

Qu'est-ce que SQLC

SQLC est un outil de développement puissant dont la fonction principale est de convertir les requêtes SQL en code Go de type sécurisé. En analysant les instructions SQL et les structures de base de données, sqlc peut générer automatiquement les structures et fonctions Go correspondantes, simplifiant considérablement le processus d'écriture de code pour les opérations de base de données.

Grâce à sqlc, les développeurs peuvent se concentrer sur l'écriture de requêtes SQL et laisser le travail fastidieux de génération de code Go à l'outil, accélérant ainsi le processus de développement et améliorant la qualité du code.

Implémentation des transactions SQLC

Le code généré par Sqlc contient généralement une structure Requêtes, qui encapsule toutes les opérations de base de données. Cette structure implémente une interface générale Querier, qui définit toutes les méthodes de requête de base de données.

La clé est que la fonction New générée par sqlc peut accepter n'importe quel objet qui implémente l'interface DBTX, y compris *sql.DB et *sql.Tx.

Le cœur de la mise en œuvre des transactions consiste à utiliser le polymorphisme de l’interface de Go. Lorsque vous devez effectuer des opérations dans une transaction, vous créez un objet *sql.Tx, puis le transmettez à la fonction New pour créer une nouvelle instance de requêtes. Cette instance effectuera toutes les opérations dans le contexte d'une transaction.

Supposons que nous nous connections à la base de données Postgres via pgx et initialisons les requêtes avec le code suivant.

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)
}

Encapsulation des transactions

Le code suivant est une encapsulation intelligente des transactions SQLc, qui simplifie le processus d'utilisation des transactions de base de données dans Go. La fonction accepte un contexte et une fonction de rappel comme paramètres. Cette fonction de rappel est l'opération spécifique que l'utilisateur souhaite effectuer dans la transaction.

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)
}

La fonction démarre d'abord une nouvelle transaction, puis retarde son exécution pour garantir que la transaction sera finalement annulée à moins qu'elle ne soit explicitement validée. Il s'agit d'un mécanisme de sécurité pour empêcher les transactions inachevées d'occuper des ressources. Ensuite, la fonction appelle le rappel fourni par l'utilisateur, en transmettant un objet de requête avec un contexte de transaction, permettant à l'utilisateur d'effectuer les opérations de base de données requises au sein de la transaction.

Si le rappel s'exécute avec succès sans erreur, la fonction valide la transaction. Toute erreur survenant au cours du processus entraînera l’annulation de la transaction. Cette méthode garantit non seulement la cohérence des données, mais simplifie également grandement la gestion des erreurs.

L'élégance de cette encapsulation est qu'elle cache une logique complexe de gestion des transactions derrière un simple appel de fonction. Les utilisateurs peuvent se concentrer sur l'écriture de la logique métier sans se soucier du démarrage, de la validation ou de l'annulation de transactions.

L'utilisation de ce code est assez intuitive. Vous pouvez appeler la fonction db.WithTransaction là où vous devez effectuer une transaction, en transmettant une fonction comme paramètre qui définit toutes les opérations de base de données que vous souhaitez effectuer dans la transaction.

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")
}

Dans cet exemple, nous créons un utilisateur et une publication dans une transaction. Si une opération échoue, la totalité de la transaction est annulée. Si toutes les opérations réussissent, la transaction est validée.

L'avantage de cette approche est que vous n'avez pas besoin de gérer manuellement le démarrage, la validation ou l'annulation de la transaction, tout cela est géré par la fonction db.WithTransaction. Vous devez uniquement vous concentrer sur les opérations de base de données réelles effectuées dans le cadre de la transaction. Cela simplifie grandement le code et réduit le risque d'erreurs.

Autres emballages

La méthode d'emballage mentionnée ci-dessus n'est pas sans défauts.

Cette simple encapsulation de transaction présente des limites lorsqu'il s'agit de transactions imbriquées. En effet, il crée une nouvelle transaction à chaque fois au lieu de vérifier si vous en êtes déjà dans une.

Afin d'implémenter le traitement des transactions imbriquées, nous devons obtenir l'objet de transaction actuel, mais l'objet de transaction actuel est caché dans sqlc.Queries, nous devons donc étendre sqlc.Queries.

La structure qui étend sqlc.Queries est créée par nous en tant que référentiels. Elle étend *sqlc.Queries et ajoute un nouveau pool d'attributs, qui est un pointeur de type pgxpool.Pool.

type Repositories struct {
    *sqlc.Queries
    pool *pgxpool.Pool
}

func NewRepositories(pool *pgxpool.Pool) *Repositories {
    return &Repositories{
        pool:    pool,
        Queries: sqlc.New(pool),
    }
}

Mais lorsque nous commencerons à écrire du code, nous constaterons que *pgxpool.Pool ne satisfait pas l'interface pgx.Tx. C'est parce que *pgxpool.Pool ne contient pas les méthodes Rollback et Commit. Méthode, afin de résoudre ce problème, nous continuons à étendre les référentiels, à y ajouter un nouvel attribut tx et à y ajouter une nouvelle méthode 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),
    }
}

Maintenant, il y a à la fois des attributs pool et tx dans notre structure de référentiels. Cela peut ne pas sembler très élégant. Pourquoi ne pouvons-nous pas extraire un type TX unifié ? En fait, c'est la raison mentionnée ci-dessus, c'est-à-dire *pgxpool ? .Pool Il n'existe qu'un moyen de démarrer une transaction, mais pas de la terminer. Une façon de résoudre ce problème consiste à créer une autre structure RepositoriesTX et à y stocker pgx.Tx au lieu de *pgxpool.Pool, mais cela peut entraîner des problèmes. de nouvelles questions. L'une d'elles est que nous devrons peut-être implémenter la méthode WithTransaction pour les deux respectivement. Quant à l'autre question, nous en parlerons plus tard. Maintenant, implémentons d'abord la méthode WithTransaction des référentiels.

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 实例。

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn