データベース
@zeltjs/db パッケージは、AsyncLocalStorageを使った自動トランザクション伝播により、ORMに依存しないデータベース抽象化を提供します。
インストール
pnpm add @zeltjs/db
概要
Zeltのデータベース抽象化は、よくある問題を解決します: トランザクションオブジェクトを明示的に渡すことなく、service層を通じてトランザクションを伝播させることです。Node.jsのAsyncLocalStorageを使うことで、トランザクションはasyncの呼び出しチェーンを自動的に流れます。
データベースServiceの作成
DatabaseService を継承して、あなたのORMを統合します:
import { Config, Env, Injectable, inject } from '@zeltjs/core';
import { DatabaseService } from '@zeltjs/db';
import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
@Config
export class DatabaseConfig {
constructor(private env = inject(Env)) {}
get url(): string {
return this.env.getStringOrThrow('DATABASE_URL');
}
}
type DrizzleReady = { client: PostgresJsDatabase; sql: postgres.Sql };
@Injectable()
export class DrizzleService extends DatabaseService<PostgresJsDatabase, DrizzleReady> {
constructor(private config = inject(DatabaseConfig)) {
super();
}
async setup(): Promise<DrizzleReady> {
const sql = postgres(this.config.url);
return { client: drizzle(sql), sql };
}
transaction<T>(
client: PostgresJsDatabase,
fn: (tx: PostgresJsDatabase) => Promise<T>,
): Promise<T> {
return client.transaction(fn);
}
async shutdown(): Promise<void> {
await this.ready.sql.end();
}
}
主なポイント:
setup()—shutdown()が必要とするクライアントとハンドル(例: 生の接続プール)を作成する。戻り値はthis.readyに封印されるtransaction()— トランザクション内で関数を実行shutdown()—this.readyに保持された接続をグレースフルシャットダウン時にクローズ
データベースServiceの使用
直接利用
serviceをinjectしてクライアントにアクセスします:
@Injectable()
export class UserRepository {
constructor(private db = inject(DrizzleService)) {}
async findAll() {
return this.db.client.select().from(users);
}
async create(name: string, email: string) {
return this.db.client.insert(users).values({ name, email });
}
}
client プロパティは自動的に以下を返します:
- トランザクション内であればトランザクションクライアント
- そうでなければ元のクライアント
トランザクションデコレータ
データベースserviceのためのデコレータを作成します:
export const Transaction = createTransactionDecorator(DrizzleService);
トランザクション内で実行すべきメソッドに適用します:
@Injectable()
export class OrderService {
constructor(
private orderRepo = inject(OrderRepository),
private inventoryRepo = inject(InventoryRepository),
) {}
@Transaction()
async placeOrder(userId: string, items: OrderItem[]) {
const order = await this.orderRepo.create(userId, items);
for (const item of items) {
await this.inventoryRepo.decrement(item.productId, item.quantity);
}
return order;
}
}
placeOrder 内のすべてのrepository呼び出しは自動的に同じトランザクションを使います。いずれかの操作が失敗すると、トランザクション全体がロールバックされます。
トランザクションミドルウェア
リクエストスコープのトランザクションには、ミドルウェアを使います:
export const TransactionMiddleware = createTransactionMiddleware(DrizzleService);
controllerに適用します:
@Controller('/orders')
@UseMiddleware(TransactionMiddleware)
export class OrderController {
constructor(private orderService = inject(OrderService)) {}
@Post('/')
async create(req = request(PlaceOrderBody)) {
const data = await req.body();
return this.orderService.placeOrder(data.userId, data.items);
}
}
このcontrollerへのすべてのリクエストはトランザクション内で実行されます。
トランザクションの伝播
トランザクションはasyncの呼び出しチェーンを自動的に伝播します:
@Injectable()
export class PaymentService {
constructor(
private ledgerRepo = inject(LedgerRepository),
private notificationService = inject(NotificationService),
) {}
@Transaction()
async processPayment(orderId: string, amount: number) {
await this.ledgerRepo.debit(orderId, amount);
await this.notificationService.sendReceipt(orderId);
}
}
@Injectable()
export class OrderService {
constructor(
private orderRepo = inject(OrderRepository),
private paymentService = inject(PaymentService),
) {}
@Transaction()
async completeOrder(orderId: string) {
await this.orderRepo.markComplete(orderId);
await this.paymentService.processPayment(orderId, 100);
}
}
completeOrder が processPayment を呼び出すとき、両方とも同じトランザクション内で実行されます — 内側の @Transaction() は新しいトランザクションを開始するのではなく、既存のトランザクションに合流します。
ライフサイクル統合
DatabaseService はZeltのライフサイクルシステムと統合します。DrizzleService は通常の injectable であり、誰かが「登録」するものではありません。何か(例えば repository)が最初にそれを inject した時点で自動的に構築され、LifecycleManager に登録されます。アプリは実際にリクエストを処理し始める前に、未実行の lifecycle をすべて実行します:
- 起動時に
setup()が呼ばれ、その戻り値がthis.readyに封印される - シャットダウン時に
shutdown()が呼ばれる
APIリファレンス
DatabaseService
| プロパティ/メソッド | 説明 |
|---|---|
client | 現在のデータベースクライアント(トランザクション対応) |
ready | Protected: setup() の戻り値から封印された ReadyValue。起動前にアクセスすると例外を投げる |
setup() | 抽象: クライアントとシャットダウンに必要なハンドルを作成する。戻り値は ready に封印される |
transaction(client, fn) | 抽象: トランザクション内で関数を実行 |
withTransaction(fn) | 新規または既存のトランザクション内で関数を実行 |
shutdown() | 抽象: シャットダウン時に接続をクローズ |
ファクトリ関数
| 関数 | 説明 |
|---|---|
createTransactionDecorator(Service) | @Transaction() デコレータを作成する |
createTransactionMiddleware(Service) | トランザクションミドルウェアクラスを作成する |