Skip to content

Commit 30e0315

Browse files
hideyukiMORIhideyukiMORIclaude
authored
feat: Field Trial 5 — transactional() DX 検証(ウォレット送金 API)(#93) (#96)
SqlAlchemyTransactionManager.transactional() を送金 API で検証。 _in_tx パターンでリポジトリと組み合わせ、原子性(失敗時ロールバック)を実証。 摩擦点 F-1(#94: py.typed)、F-2(#95: _in_tx ドキュメント) を記録。 Co-authored-by: hideyukiMORI <info.xion.cc@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent eeef4b2 commit 30e0315

2 files changed

Lines changed: 156 additions & 2 deletions

File tree

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
# Field Trial 5 — wallet: transactional() DX 検証
2+
3+
## Date
4+
5+
2026-05-19
6+
7+
## Baseline
8+
9+
- nene2-python v0.1.0 (`uv add git+https://github.com/hideyukiMORI/nene2-python.git`)
10+
- Python 3.14.5(uv managed)
11+
- プロジェクト: **wallet** — ウォレット送金 JSON API
12+
- エンティティ: `Account`(id, name, balance_cents)、`Transfer`(id, from/to, amount_cents)
13+
- 5 エンドポイント(GET/POST accounts, POST transfer, GET transfers)
14+
- **`SqlAlchemyTransactionManager.transactional()`** ← FT1〜FT4 で未検証のコア機能
15+
16+
## Goal
17+
18+
1. `transactional()` コールバックパターンの実用 DX を確認する
19+
2. `transactional()` と Repository パターンの組み合わせ方を検証する
20+
3. 原子性(失敗時ロールバック)が実際に機能することを確認する
21+
22+
---
23+
24+
## Steps Taken
25+
26+
### 1. プロジェクト初期化・インストール
27+
28+
問題なし。FT1〜FT4 で確立されたパターン通り。
29+
30+
### 2. `_in_tx` パターンの設計
31+
32+
`transactional()` コールバック内でリポジトリ操作を行うため、executor を受け取る専用メソッドを定義した(**F-2**):
33+
34+
```python
35+
class AccountRepositoryInterface(ABC):
36+
def find_by_id(self, account_id: int) -> Account | None: ...
37+
38+
# トランザクション内専用 — transactional() コールバック内から呼ぶ
39+
def find_by_id_in_tx(
40+
self, executor: DatabaseQueryExecutorInterface, account_id: int
41+
) -> Account | None: ...
42+
def update_balance_in_tx(
43+
self, executor: DatabaseQueryExecutorInterface, account_id: int, delta_cents: int
44+
) -> None: ...
45+
```
46+
47+
UseCase での使い方:
48+
49+
```python
50+
class TransferUseCase:
51+
def execute(self, input_: TransferInput) -> Transfer:
52+
def _run(executor: DatabaseQueryExecutorInterface) -> Transfer:
53+
source = self._accounts.find_by_id_in_tx(executor, input_.from_account_id)
54+
if source is None:
55+
raise AccountNotFoundException(input_.from_account_id)
56+
target = self._accounts.find_by_id_in_tx(executor, input_.to_account_id)
57+
if target is None:
58+
raise AccountNotFoundException(input_.to_account_id)
59+
if source.balance_cents < input_.amount_cents:
60+
raise InsufficientBalanceException(...)
61+
62+
self._accounts.update_balance_in_tx(executor, input_.from_account_id, -input_.amount_cents)
63+
self._accounts.update_balance_in_tx(executor, input_.to_account_id, input_.amount_cents)
64+
return self._transfers.create_in_tx(executor, ...)
65+
66+
return self._tx.transactional(_run)
67+
```
68+
69+
### 3. transactional() 動作確認
70+
71+
```
72+
POST /accounts {"name": "Alice", "initial_balance_cents": 10000} → 201
73+
POST /accounts {"name": "Bob", "initial_balance_cents": 5000} → 201
74+
POST /accounts/1/transfer {"to_account_id": 2, "amount_cents": 3000} → 201
75+
GET /accounts/1 → balance_cents: 7000 ✓ (10000 - 3000)
76+
GET /accounts/2 → balance_cents: 8000 ✓ (5000 + 3000)
77+
```
78+
79+
### 4. 原子性の確認
80+
81+
存在しない転送先を指定 → Alice の残高が変わらないことを確認:
82+
83+
```
84+
POST /accounts/1/transfer {"to_account_id": 9999, "amount_cents": 1000} → 404
85+
GET /accounts/1 → balance_cents: 7000 ✓ (変化なし = ロールバック成功)
86+
```
87+
88+
`engine.begin()` コンテキストマネージャーが例外で自動ロールバックすることを実証。
89+
90+
### 5. エラーレスポンス確認
91+
92+
```
93+
POST /accounts/1/transfer {"amount_cents": 9999} (残高 7000 に対して)
94+
→ 422 {"type": ".../insufficient-balance", "title": "Insufficient Balance", ...} ✓
95+
```
96+
97+
### 6. mypy 実行(F-1 発見)
98+
99+
```
100+
Skipping analyzing "nene2.http": module is installed, but missing library stubs or py.typed marker
101+
```
102+
103+
`ignore_missing_imports = true` + `warn_return_any = false` で回避。詳細は F-1 参照。
104+
105+
---
106+
107+
## Friction Points
108+
109+
### F-1 nene2-python に py.typed マーカーがなく型情報が失われる
110+
111+
**severity**: 中
112+
**type**: パッケージ設定不備
113+
114+
`uv run mypy src/` を実行すると nene2 モジュールの型情報が読み込まれない。
115+
PEP 561 の `py.typed` マーカーファイルが `src/nene2/` に存在しないため。
116+
117+
回避策として以下を `[tool.mypy]` に追加した:
118+
```toml
119+
ignore_missing_imports = true
120+
warn_return_any = false
121+
```
122+
123+
本来これらは不要なはず。nene2 には型注釈が完備されており、`py.typed` を追加するだけで解決する。
124+
125+
**Follow-up**: `src/nene2/py.typed` を追加して PEP 561 対応する。
126+
127+
### F-2 transactional() とリポジトリを組み合わせるパターンがドキュメントなし
128+
129+
**severity**: 中
130+
**type**: ドキュメント不足
131+
132+
`transactional(callback)` でコールバック内からリポジトリを呼ぶ方法が非自明。
133+
実装してみると「`_in_tx` サフィックス付きメソッドで executor を受け取る」パターンが自然だと分かったが、ガイドがない。
134+
135+
**Follow-up**: `docs/reference/framework-modules.md``docs/how-to/sqlalchemy-repository.md``transactional()` 実践パターンを追記する。
136+
137+
---
138+
139+
## Summary
140+
141+
| ID | 摩擦 | 深刻度 | 種別 | Follow-up Issue |
142+
|-----|--------------------------------------------------------------|--------|------------------|-----------------|
143+
| F-1 | `py.typed` マーカーなしで mypy 型情報が失われる || パッケージ設定 | #94 |
144+
| F-2 | `transactional()` + リポジトリの `_in_tx` パターンが非文書化 || ドキュメント不足 | #95 |
145+
146+
`transactional()` 自体は設計通り動作し、原子性も確認。
147+
`_in_tx` パターンは一度設計すると明快で、InMemory 実装でも再現しやすい(テスト容易性◎)。
148+
149+
次回 FT6 は以下のいずれかを推奨:
150+
- F-1/F-2 修正後に **PyPI 公開フロー**の検証
151+
- WebSocket サポートの検討(FastAPI の WebSocket エンドポイントをフレームワーク観点で評価)

docs/roadmap.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,11 @@ PHP 版追跡・Python 固有の強化:
130130
- [x] Field Trial 1: InMemory CRUD + git+ インストール検証 (#67)
131131
- [x] Field Trial 2: SQLite 永続化リポジトリ DX 検証 (#72)
132132
- [x] Field Trial 3: Bearer Token 認証 + MCP stdio DX 検証 (#80)
133-
- [ ] Field Trial 4: MCP + SQLite 共有 / ApiKey / CORS 検証
134-
- [ ] PyPI パッケージ公開(FT4 完了後)
133+
- [x] Field Trial 4: MCP + SQLite 共有 / ApiKey / CORS 検証 (#89)
134+
- [x] Field Trial 5: transactional() DX 検証(ウォレット送金 API)(#93)
135+
- [ ] `py.typed` 追加で PEP 561 対応 (#94)
136+
- [ ] `transactional()` + `_in_tx` パターンをドキュメント化 (#95)
137+
- [ ] PyPI パッケージ公開(FT5 完了後)
135138
- [ ] WebSocket サポート検討
136139

137140
---

0 commit comments

Comments
 (0)