-
Notifications
You must be signed in to change notification settings - Fork 0
feat: AsyncLocalStorageを用いたUnit of Work基盤を導入 #126
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
Mel-906
wants to merge
4
commits into
develop
Choose a base branch
from
feature/unit-of-work
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
bc8b1b8
feat: AsyncLocalStorageを用いたUnit of Work基盤を導入
Mel-906 ba137b9
refactor: getDb/createDb/DrizzleDb を getClient/createClient/DrizzleCl…
Mel-906 a90e539
refactor: DrizzleClientをPgDatabase型で定義しトランザクションの強制キャストを除去
Mel-906 21d0543
fix: resolve drizzle client merge conflict
Mel-906 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| /** | ||
| * 一連の操作をまとめて扱うためのインターフェース | ||
| * 具体的な実装(DBトランザクション等)はInfrastructure層が担当する | ||
| */ | ||
| export interface UnitOfWork { | ||
| run<T>(fn: () => Promise<T>): Promise<T>; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,32 @@ | ||
| import type { UnitOfWork } from "../UnitOfWork"; | ||
|
|
||
| /** | ||
| * ユースケースの基底抽象クラス | ||
| * TInputはユースケースへの入力型 | ||
| * TOutputはユースケースからの出力型 | ||
| */ | ||
| export abstract class IUseCase<Input, Output> { | ||
| /** | ||
| * ユースケースのビジネスロジックを実行する | ||
| * トランザクションなしで実行されるため、読み取り専用のUseCaseはこちらを使う | ||
| * 複数のDB書き込みを安全にまとめたい場合は run() を使うこと | ||
| */ | ||
| abstract execute(input: Input): Promise<Output>; | ||
|
|
||
| /** | ||
| * UnitOfWorkで囲んでexecuteを実行する | ||
| * トランザクション内で全てのDB操作がまとめて成功/失敗する | ||
| * 複数の集約をまたぐ書き込み操作がある場合に使用する | ||
| * | ||
| * 注意: | ||
| * - execute()内で外部API呼び出し(Discord API等)を行うと、 | ||
| * その応答待ちの間トランザクションが開きっぱなしになる | ||
| * 外部連携はトランザクション完了後に行うこと | ||
| * - トランザクションがロールバックされてもメモリ上のドメインオブジェクトは | ||
| * 元に戻らない。execute()内で取得・変更したドメインオブジェクトを | ||
| * ロールバック後に再利用しないこと | ||
| */ | ||
| async run(input: Input, unitOfWork: UnitOfWork): Promise<Output> { | ||
| return unitOfWork.run(() => this.execute(input)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| import type { UnitOfWork } from "#application/UnitOfWork"; | ||
| import { runInTransaction } from "./client"; | ||
|
|
||
| /** | ||
| * UnitOfWorkのDrizzle実装 | ||
| * AsyncLocalStorageを利用して、トランザクションを処理の流れ全体で共有する | ||
| */ | ||
| export class DrizzleUnitOfWork implements UnitOfWork { | ||
| async run<T>(fn: () => Promise<T>): Promise<T> { | ||
| return runInTransaction(fn); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,24 +1,54 @@ | ||
| /// <reference types="node" /> | ||
| import { AsyncLocalStorage } from "node:async_hooks"; | ||
| import type { PgDatabase } from "drizzle-orm/pg-core"; | ||
| import { drizzle } from "drizzle-orm/postgres-js"; | ||
| import type { PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js/session"; | ||
| import postgres, { type Sql } from "postgres"; | ||
| import * as schema from "./schema"; | ||
|
|
||
| let client: Sql | null = null; | ||
| export type DrizzleClient = PgDatabase<PostgresJsQueryResultHKT, typeof schema>; | ||
|
|
||
| function getClient(): Sql { | ||
| if (!client) { | ||
| let sqlClient: Sql | null = null; | ||
|
|
||
| function getSqlClient(): Sql { | ||
| if (!sqlClient) { | ||
| const connectionString = process.env.DATABASE_URL; | ||
| if (!connectionString) { | ||
| throw new Error("DATABASE_URL environment variable is not set"); | ||
| } | ||
| // Supabase の Transaction pool mode は prepared statement をサポートしないため無効化 | ||
| client = postgres(connectionString, { prepare: false }); | ||
| sqlClient = postgres(connectionString, { prepare: false }); | ||
| } | ||
| return client; | ||
| return sqlClient; | ||
| } | ||
|
|
||
| function createClient(): DrizzleClient { | ||
| return drizzle(getSqlClient(), { schema }); | ||
| } | ||
|
|
||
| export function getDb() { | ||
| return drizzle(getClient(), { schema }); | ||
| const transactionContext = new AsyncLocalStorage<DrizzleClient>(); | ||
|
|
||
| /** | ||
| * Drizzleクライアントを取得する | ||
| * トランザクション中であればそのトランザクションを返し、 | ||
| * そうでなければ新しいクライアントを返す | ||
| */ | ||
| export function getClient(): DrizzleClient { | ||
| const tx = transactionContext.getStore(); | ||
| if (tx) return tx; | ||
| return createClient(); | ||
| } | ||
|
|
||
| export type DrizzleDb = ReturnType<typeof getDb>; | ||
| /** | ||
| * トランザクション内で処理を実行する | ||
| * すでにトランザクション中であればそのまま実行する(ネストしない) | ||
| */ | ||
| export function runInTransaction<T>(fn: () => Promise<T>): Promise<T> { | ||
| if (transactionContext.getStore()) { | ||
| return fn(); | ||
| } | ||
| const db = createClient(); | ||
| return db.transaction(async (tx) => { | ||
| return transactionContext.run(tx, fn); | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
この設計では、execute() と run() のどちらを使うかを呼び出し側が判断します。もし複数のテーブルに書き込むようなUseCaseで execute() を使ってしまうと、途中で失敗したときに一部の書き込みだけが残り、データの整合性が壊れます。しかしこのミスはエラーにならず、正常に動いているように見えるため、問題が起きるまで気づけません。
その代わりに、execute() 自体がトランザクションを開始するようにしてはどうでしょうか。
全てのUseCaseは execute() を呼ぶだけでトランザクション内で実行されるようにするということです。
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@KinjiKawaguchi
execute()にトランザクション開始を組み込む案だと、各UseCaseが実装しているメソッドの書き換え(35個)が必要になるのではと思います。
run()のみを入口にすれば、execute()の誤用も解消でき、トランザクションでの保護も行えます。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Mel-906 ご返信ありがとうございます。
execute()自体をトランザクション境界にする案を、もう少し具体的に説明させてください。基底クラスをこう変えてみるのはどうでしょうか?
こうすると、各UseCaseでは
executeをprotected _executeにリネームするだけで済みます。中身の変更はありません。例えば、
ファサード層は
execute()をそのまま呼び続けるので変更不要です。run()を唯一の入口にする案でも、execute()を外から呼べなくするために結局35個のUseCaseにprotectedを付ける変更は発生します。さらにファサード層でも全箇所execute()→run(input, uow)への書き換えとUoWの生成が必要になるので、変更量はむしろ多くなります。ちなみに
protectedはそのクラスとサブクラスからだけアクセスできる修飾子で、外部から_execute()を直接呼ぶとコンパイルエラーになります。詳しくはこちら:https://typescriptbook.jp/reference/object-oriented/class/access-modifiers#protected