Maison  >  Article  >  interface Web  >  Comment écrire des appels de base de données transactionnelle en TypeScript

Comment écrire des appels de base de données transactionnelle en TypeScript

DDD
DDDoriginal
2024-11-06 19:25:03959parcourir

How To Write Transactional Database Calls in TypeScript

Si vous écrivez des services Web, il y a de fortes chances que vous interagiez avec une base de données. Parfois, vous devrez apporter des modifications qui doivent être appliquées de manière atomique : soit toutes réussissent, soit aucune ne réussit. C'est là que les transactions entrent en jeu. Dans cet article, je vais vous montrer comment implémenter des transactions dans votre code pour éviter les problèmes de fuites d'abstractions.

Exemple

Un exemple courant est le traitement des paiements :

  • Vous devez obtenir le solde de l'utilisateur et vérifier s'il est suffisant.
  • Ensuite, vous mettez à jour le solde et l'enregistrez.

Structure

En général, votre application comportera deux modules pour séparer la logique métier du code lié à la base de données.

Module de référentiel

Ce module gère toutes les opérations liées à la base de données, telles que les requêtes SQL. Ci-dessous, nous définissons deux fonctions :

  • get_balance — Récupère le solde de l'utilisateur de la base de données.
  • set_balance — Met à jour le solde de l'utilisateur.
import { Injectable } from '@nestjs/common';
import postgres from 'postgres';

@Injectable()
export class BillingRepository {
  constructor(
    private readonly db_connection: postgres.Sql,
  ) {}

  async get_balance(customer_id: string): Promise<number | null> {
    const rows = await this.db_connection`
      SELECT amount FROM balances 
      WHERE customer_id=${customer_id}
    `;
    return (rows[0]?.amount) ?? null;
  }

  async set_balance(customer_id: string, amount: number): Promise<void> {
    await this.db_connection`
      UPDATE balances 
      SET amount=${amount} 
      WHERE customer_id=${customer_id}
    `;
  }
}

Module de services

Le module de service contient une logique métier, telle que la récupération du solde, sa validation et l'enregistrement du solde mis à jour.

import { Injectable } from '@nestjs/common';
import { BillingRepository } from 'src/billing/billing.repository';

@Injectable()
export class BillingService {
  constructor(
    private readonly billing_repository: BillingRepository,
  ) {}

  async bill_customer(customer_id: string, amount: number) {
    const balance = await this.billing_repository.get_balance(customer_id);

    // The balance may change between the time of this check and the update.
    if (balance === null || balance < amount) {
      return new Error('Insufficient funds');
    }

    await this.billing_repository.set_balance(customer_id, balance - amount);
  }
}

Dans la fonction bill_customer, on récupère d'abord le solde de l'utilisateur à l'aide de get_balance. Ensuite, nous vérifions si le solde est suffisant et le mettons à jour avec set_balance.

Transactions

Le problème avec le code ci-dessus est que le solde peut changer entre le moment où il est récupéré et celui où il est mis à jour. Pour éviter cela, nous devons utiliser des transactions. Vous pouvez gérer cela de deux manières :

  • Intégrer la logique métier dans le module de référentiel : cette approche associe les règles métier aux opérations de base de données, ce qui rend les tests plus difficiles.
  • Utiliser une transaction dans le module de service : cela pourrait conduire à des fuites d'abstractions, car le module de service devrait gérer explicitement les sessions de base de données.

Au lieu de cela, je recommande une approche plus propre.

Code transactionnel

Un bon moyen de gérer les transactions est de créer une fonction qui encapsule un rappel dans une transaction. Cette fonction fournit un objet de session qui n'expose pas de détails internes inutiles, empêchant ainsi les abstractions qui fuient. L'objet de session est transmis à toutes les fonctions liées à la base de données au sein de la transaction.

Voici comment vous pouvez le mettre en œuvre :

import { Injectable } from '@nestjs/common';
import postgres, { TransactionSql } from 'postgres';

export type SessionObject = TransactionSql<Record<string, unknown>>;

@Injectable()
export class BillingRepository {
  constructor(
    private readonly db_connection: postgres.Sql,
  ) {}

  async run_in_session<T>(cb: (sql: SessionObject) => T | Promise<T>) {
    return await this.db_connection.begin((session) => cb(session));
  }

  async get_balance(
    customer_id: string, 
    session: postgres.TransactionSql | postgres.Sql = this.db_connection
  ): Promise<number | null> {
    const rows = await session`
      SELECT amount FROM balances 
      WHERE customer_id=${customer_id}
    `;
    return (rows[0]?.amount) ?? null;
  }

  async set_balance(
    customer_id: string, 
    amount: number, 
    session: postgres.TransactionSql | postgres.Sql = this.db_connection
  ): Promise<void> {
    await session`
      UPDATE balances 
      SET amount=${amount} 
      WHERE customer_id=${customer_id}
    `;
  }
}

Dans cet exemple, la fonction run_in_session démarre une transaction et exécute un rappel à l'intérieur de celle-ci. Le type SessionObject résume la session de base de données pour éviter toute fuite de détails internes. Toutes les fonctions liées à la base de données acceptent désormais un objet de session, garantissant qu'elles peuvent participer à la même transaction.

Module de service mis à jour

Le module de service est mis à jour pour tirer parti des transactions. Voici à quoi cela ressemble :

import { Injectable } from '@nestjs/common';
import postgres from 'postgres';

@Injectable()
export class BillingRepository {
  constructor(
    private readonly db_connection: postgres.Sql,
  ) {}

  async get_balance(customer_id: string): Promise<number | null> {
    const rows = await this.db_connection`
      SELECT amount FROM balances 
      WHERE customer_id=${customer_id}
    `;
    return (rows[0]?.amount) ?? null;
  }

  async set_balance(customer_id: string, amount: number): Promise<void> {
    await this.db_connection`
      UPDATE balances 
      SET amount=${amount} 
      WHERE customer_id=${customer_id}
    `;
  }
}

Dans la fonction bill_customer_transactional, nous appelons run_in_session et passons un rappel avec l'objet session en paramètre, puis nous passons ce paramètre à chaque fonction du référentiel que nous appelons. Cela garantit que get_balance et set_balance s'exécutent dans la même transaction. Si le solde change entre les deux appels, la transaction échouera, préservant ainsi l'intégrité des données.

Conclusion

L'utilisation efficace des transactions garantit la cohérence des opérations de votre base de données, en particulier lorsque plusieurs étapes sont impliquées. L'approche que j'ai décrite vous aide à gérer les transactions sans fuite d'abstractions, ce qui rend votre code plus maintenable. Essayez d'implémenter ce modèle dans votre prochain projet pour garder votre logique propre et vos données en sécurité !


Merci d'avoir lu !

?N'oubliez pas de liker si l'article vous a plu ?

Contacts
Si vous aimez cet article, n'hésitez pas à vous connecter sur LinkedIn et à me suivre sur Twitter.

Abonnez-vous à ma liste de diffusion : https://sergedevs.com

Assurez-vous d'aimer et de suivre ?

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