TypeScript에서 트랜잭션 데이터베이스 호출을 작성하는 방법

DDD
풀어 주다: 2024-11-06 19:25:03
원래의
956명이 탐색했습니다.

How To Write Transactional Database Calls in TypeScript

웹 서비스를 작성한다면 데이터베이스와 상호작용할 가능성이 높습니다. 때로는 원자적으로 적용해야 하는 변경 사항을 적용해야 하는 경우가 있습니다. 모두 성공하거나 모두 성공하지 못합니다. 여기가 트랜잭션이 필요한 곳입니다. 이 기사에서는 누출 추상화 문제를 방지하기 위해 코드에서 트랜잭션을 구현하는 방법을 보여 드리겠습니다.

일반적인 예는 결제 처리입니다.

  • 사용자의 잔액을 확보하여 충분한지 확인해야 합니다.
  • 그런 다음 잔액을 업데이트하고 저장합니다.

구조

일반적으로 애플리케이션에는 비즈니스 로직을 데이터베이스 관련 코드와 분리하는 두 개의 모듈이 있습니다.

리포지토리 모듈

이 모듈은 SQL 쿼리 등 모든 데이터베이스 관련 작업을 처리합니다. 아래에서는 두 가지 함수를 정의합니다.

  • get_balance — 데이터베이스에서 사용자 잔액을 검색합니다.
  • set_balance — 사용자의 잔액을 업데이트합니다.
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}
    `;
  }
}
로그인 후 복사
로그인 후 복사

서비스 모듈

서비스 모듈에는 잔액 가져오기, 유효성 검사, 업데이트된 잔액 저장과 같은 비즈니스 로직이 포함되어 있습니다.

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);
  }
}
로그인 후 복사

bills_customer 함수에서는 먼저 get_balance를 사용하여 사용자의 잔액을 검색합니다. 그런 다음 잔액이 충분한지 확인하고 set_balance로 업데이트합니다.

업무

위 코드의 문제점은 가져오는 시간과 업데이트되는 시간 사이에 잔액이 변경될 수 있다는 것입니다. 이를 방지하려면 트랜잭션을 사용해야 합니다. 이 문제는 두 가지 방법으로 처리할 수 있습니다.

  • 저장소 모듈에 비즈니스 로직 삽입: 이 접근 방식은 비즈니스 규칙과 데이터베이스 작업을 결합하여 테스트를 더 어렵게 만듭니다.
  • 서비스 모듈에서 트랜잭션 사용: 서비스 모듈이 데이터베이스 세션을 명시적으로 관리해야 하므로 추상화가 누출될 수 있습니다.

대신 더 깔끔한 접근 방식을 권장합니다.

거래 코드

트랜잭션을 처리하는 좋은 방법은 트랜잭션 내에서 콜백을 래핑하는 함수를 만드는 것입니다. 이 기능은 불필요한 내부 세부 정보를 노출하지 않는 세션 개체를 제공하여 추상화 누출을 방지합니다. 세션 개체는 트랜잭션 내의 모든 데이터베이스 관련 기능에 전달됩니다.

구현 방법은 다음과 같습니다.

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}
    `;
  }
}
로그인 후 복사

이 예에서 run_in_session 함수는 트랜잭션을 시작하고 그 안에서 콜백을 실행합니다. SessionObject 유형은 내부 세부 정보 유출을 방지하기 위해 데이터베이스 세션을 추상화합니다. 이제 모든 데이터베이스 관련 기능이 세션 개체를 허용하므로 동일한 트랜잭션에 참여할 수 있습니다.

업데이트된 서비스 모듈

거래를 활용할 수 있도록 서비스 모듈이 업데이트되었습니다. 그 모습은 다음과 같습니다.

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}
    `;
  }
}
로그인 후 복사
로그인 후 복사

bill_customer_transactional 함수에서는 run_in_session을 호출하고 세션 객체와 함께 콜백을 매개변수로 전달한 다음 이 매개변수를 우리가 호출하는 저장소의 모든 함수에 전달합니다. 이렇게 하면 get_balance와 set_balance가 모두 동일한 트랜잭션 내에서 실행됩니다. 두 호출 사이에 잔액이 변경되면 트랜잭션이 실패하고 데이터 무결성이 유지됩니다.

결론

트랜잭션을 효과적으로 사용하면 특히 여러 단계가 포함될 때 데이터베이스 작업의 일관성이 유지됩니다. 제가 설명한 접근 방식을 사용하면 추상화 유출 없이 트랜잭션을 관리할 수 있어 코드 유지 관리가 더욱 쉬워집니다. 논리를 깔끔하게 유지하고 데이터를 안전하게 유지하려면 다음 프로젝트에서 이 패턴을 구현해 보세요!


읽어주셔서 감사합니다!

?글이 마음에 드셨다면 좋아요 잊지 마세요?

연락처
이 기사가 마음에 들면 주저하지 말고 LinkedIn에 연결하고 Twitter에서 나를 팔로우하세요.

내 메일링 리스트를 구독하세요: https://sergedevs.com

좋아요와 팔로우를 꼭 해주세요 ?

위 내용은 TypeScript에서 트랜잭션 데이터베이스 호출을 작성하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:dev.to
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!