1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
|
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="author" content="韩暮秋">
<meta name="subtitle" content="暮秋小屋">
<meta name="description" content="这里是暮秋小屋,思念和灵感的寄存处">
<meta name="keywords" content="韩暮秋,MuqiuHan,'Muqiu Han', 'muqiu han', muqiuhan">
<title>
如何在 DDD + CQRS 中处理跨聚合的事务 |
暮秋小屋
</title>
<link rel="icon" href="/favicon.ico">
<!-- stylesheets list from _config.yml -->
<link rel="stylesheet" href="/css/style.css">
<link rel="preload" href="/fonts/TensentypeCode.woff2" as="font" type="font/woff2" crossorigin>
<!-- scripts list from _config.yml -->
<script
src="/js/menu.js"></script>
<script
src="https://polyfill.alicdn.com/polyfill.js?features=es6"></script>
<script
id="MathJax-script"
async
src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/mathjax/3.2.0/es5/tex-mml-chtml.js"></script>
<meta name="generator" content="Hexo 6.3.0"></head>
<body>
<div class="wrapper">
<div class="header">
<div class="flex-container">
<div class="header-inner">
<div class="site-brand-container">
<a href="/">
暮秋小屋
</a>
</div>
<div id="menu-btn" class="menu-btn" onclick="toggleMenu()">
菜单
</div>
<nav class="site-nav">
<ul class="menu-list">
<li class="menu-item">
<a href="/">
主页
</a>
</li>
<li class="menu-item">
<a href="/categories/gallery/">
日记本
</a>
</li>
<li class="menu-item">
<a href="/tags/Medicine/">
泛医学
</a>
</li>
<li class="menu-item">
<a href="/tags/Technique/">
计算机
</a>
</li>
<li class="menu-item">
<a href="/tags/Life/">
生活
</a>
</li>
<li class="menu-item">
<a href="/archives">
全部
</a>
</li>
<li class="menu-item">
<a href="/about">
关于
</a>
</li>
<li class="menu-item">
<a href="/search">搜索</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="main">
<div class="flex-container">
<article id="post">
<div class="post-head">
<div class="post-info">
<div class="tag-list">
<span class="post-tag">
<a href="/tags/Technique/">
Technique
</a>
</span>
</div>
<div class="post-title">
如何在 DDD + CQRS 中处理跨聚合的事务
</div>
<span class="post-date">
May 28, 2026
</span>
</div>
<div class="post-img">
<div class="h-line-primary"></div>
</div>
</div>
<div class="post-content">
<p>假设系统里有一个模块,用来管理化合物从 hit、lead 到 candidate 的推进。</p>
<p>药化团队合成了一批化合物。生物团队上传活性、选择性、ADME、早期毒理等实验结果。项目负责人看完某个化合物的数据包后,要做一个决策:</p>
<ul>
<li>推进为候选化合物。</li>
<li>退回继续优化。</li>
<li>暂停该方向。</li>
</ul>
<p>提交这个决策时,系统通常要做这些事:</p>
<ul>
<li>写入一条候选化合物推进决策。</li>
<li>改变化合物当前研发阶段。</li>
<li>如果推进成功,创建候选药物档案。</li>
<li>关闭当前评审责任。</li>
<li>锁定本次使用的实验数据包版本。</li>
<li>写入审计事件,说明阶段变化依据哪一次数据评审。</li>
</ul>
<p>这些写入必须一起成功。不能出现决策已经提交,但化合物阶段没变, 或是候选药物档案已创建,但数据包没有锁定等问题。</p>
<p>最直接的实现,是在 Command Handler 里注入 Prisma,然后开事务:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SubmitCompoundPromotionDecisionHandler</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">prisma</span>: <span class="title class_">PrismaClient</span></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">execute</span>(<span class="params"><span class="attr">command</span>: <span class="title class_">SubmitCompoundPromotionDecision</span></span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">prisma</span>.$transaction(<span class="title function_">async</span> (tx) => {</span><br><span class="line"> <span class="keyword">const</span> decision = <span class="keyword">await</span> tx.<span class="property">compoundDecision</span>.<span class="title function_">create</span>({</span><br><span class="line"> <span class="attr">data</span>: {</span><br><span class="line"> <span class="attr">compoundId</span>: command.<span class="property">compoundId</span>,</span><br><span class="line"> <span class="attr">result</span>: command.<span class="property">result</span>,</span><br><span class="line"> <span class="attr">reason</span>: command.<span class="property">reason</span>,</span><br><span class="line"> <span class="attr">decidedBy</span>: command.<span class="property">actorUserId</span>,</span><br><span class="line"> },</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> tx.<span class="property">compound</span>.<span class="title function_">update</span>({</span><br><span class="line"> <span class="attr">where</span>: { <span class="attr">id</span>: command.<span class="property">compoundId</span> },</span><br><span class="line"> <span class="attr">data</span>: { <span class="attr">stage</span>: command.<span class="property">result</span> === <span class="string">"promote"</span> ? <span class="string">"candidate"</span> : <span class="string">"lead"</span> },</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (command.<span class="property">result</span> === <span class="string">"promote"</span>) {</span><br><span class="line"> <span class="keyword">await</span> tx.<span class="property">developmentCandidate</span>.<span class="title function_">create</span>({</span><br><span class="line"> <span class="attr">data</span>: { <span class="attr">compoundId</span>: command.<span class="property">compoundId</span> },</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> tx.<span class="property">reviewAssignment</span>.<span class="title function_">update</span>(...);</span><br><span class="line"> <span class="keyword">await</span> tx.<span class="property">auditEvent</span>.<span class="title function_">create</span>(...);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">return</span> decision;</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>但它把几个层次混在了一起。</p>
<p>Command Handler 本来应该编排业务流程,但现在它编排了数据库表的更新顺序。</p>
<p>化合物阶段变化本来应该由 <code>Compound</code> 聚合判断。比如只有 <code>lead_optimization</code> 阶段的化合物才能推进到 <code>candidate</code>,已经暂停的化合物不能直接推进。现在这条规则可能散落在 <code>tx.compound.update</code> 前后的 if 判断里。</p>
<p>事务边界本来属于用例。现在它由具体 ORM 的 <code>$transaction</code> 暴露在 Command Handler 里。换 ORM、拆 outbox、改审计事件写法,Command Handler 都要动。</p>
<hr>
<p>如果把这段逻辑挪到 repository 里:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">compoundRepository.<span class="title function_">submitPromotionDecision</span>(command);</span><br></pre></td></tr></table></figure>
<p>这看起来把 Prisma 从 Command Handler 里拿掉了。但如果 <code>compoundRepository</code> 内部仍然创建决策、更新化合物、创建候选药物档案、关闭责任、写审计事件,那只是把混乱换了一个地方,因为 Repository 的核心职责应该只是持久化聚合,例如:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">compoundRepository.<span class="title function_">findById</span>(id);</span><br><span class="line">compoundRepository.<span class="title function_">save</span>(compound);</span><br><span class="line">compoundDecisionRepository.<span class="title function_">save</span>(decision);</span><br><span class="line">developmentCandidateRepository.<span class="title function_">save</span>(candidate);</span><br></pre></td></tr></table></figure>
<p>如果一个 <code>CompoundRepository</code> 开始保存 <code>CompoundDecision</code>、<code>DevelopmentCandidate</code>、<code>ReviewAssignment</code>、<code>AuditEvent</code>,它就不再只是 Compound 的 repository。它变成了一个跨模块事务服务,只是名字还叫 repository。</p>
<hr>
<p>所以这里我的解决方案之一是在 application 层定义一个 Unit of Work port,它表达的是一个具体业务动作:提交候选化合物推进决策。</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">SubmitCompoundPromotionDecisionUnitOfWork</span> {</span><br><span class="line"> <span class="title function_">submit</span>(<span class="attr">input</span>: {</span><br><span class="line"> <span class="attr">compoundId</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">dataPackageId</span>: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">result</span>: <span class="string">"promote"</span> | <span class="string">"optimize"</span> | <span class="string">"pause"</span>;</span><br><span class="line"> <span class="attr">reason</span>?: <span class="built_in">string</span>;</span><br><span class="line"> <span class="attr">actorUserId</span>: <span class="built_in">string</span>;</span><br><span class="line"> }): <span class="title class_">Promise</span><<span class="title class_">CompoundPromotionDecisionDto</span>>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Command Handler 依赖这个端口,而不是依赖 Prisma。</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">SubmitCompoundPromotionDecisionHandler</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"></span></span><br><span class="line"><span class="params"> <span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">unitOfWork</span>: <span class="title class_">SubmitCompoundPromotionDecisionUnitOfWork</span>,</span></span><br><span class="line"><span class="params"> </span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">execute</span>(<span class="params"><span class="attr">command</span>: <span class="title class_">SubmitCompoundPromotionDecision</span></span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">unitOfWork</span>.<span class="title function_">submit</span>({</span><br><span class="line"> <span class="attr">compoundId</span>: command.<span class="property">compoundId</span>,</span><br><span class="line"> <span class="attr">dataPackageId</span>: command.<span class="property">dataPackageId</span>,</span><br><span class="line"> <span class="attr">result</span>: command.<span class="property">result</span>,</span><br><span class="line"> <span class="attr">reason</span>: command.<span class="property">reason</span>,</span><br><span class="line"> <span class="attr">actorUserId</span>: command.<span class="property">actorUserId</span>,</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这段代码没有事务细节。它只转交命令意图。</p>
<p>具体事务放在 infrastructure:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">PrismaSubmitCompoundPromotionDecisionUnitOfWork</span></span><br><span class="line"> <span class="keyword">implements</span> <span class="title class_">SubmitCompoundPromotionDecisionUnitOfWork</span></span><br><span class="line">{</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">prisma</span>: <span class="title class_">PrismaClient</span></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">submit</span>(<span class="params"><span class="attr">input</span>: <span class="title class_">SubmitCompoundPromotionDecisionInput</span></span>) {</span><br><span class="line"> <span class="keyword">return</span> <span class="variable language_">this</span>.<span class="property">prisma</span>.$transaction(<span class="title function_">async</span> (tx) => {</span><br><span class="line"> <span class="comment">// 在这里组合 tx-bound store / repository</span></span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>这样就把“业务事务边界”和“命令入口”分开,Command Handler 不需要知道用 Prisma 还是别的 ORM。它也不需要知道事务里要调用哪些表。它只知道这个命令由一个原子用例完成。</p>
<p>但聚合仍然要负责状态规则,Unit of Work 不能变成新的数据库脚本,在事务内部,仍然应该先读取聚合,让聚合执行状态变化,再保存聚合。</p>
<p>例如 <code>Compound</code> 聚合可以这样表达规则:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Compound</span> {</span><br><span class="line"> <span class="title function_">promoteToCandidate</span>(<span class="params"><span class="attr">input</span>: {</span></span><br><span class="line"><span class="params"> dataPackageId: <span class="built_in">string</span>;</span></span><br><span class="line"><span class="params"> actorUserId: <span class="built_in">string</span>;</span></span><br><span class="line"><span class="params"> }</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">stage</span> !== <span class="string">"lead_optimization"</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">DomainError</span>(<span class="string">"只有先导优化阶段的化合物才能推进为候选化合物"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">lockedDataPackageId</span> !== <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">DomainError</span>(<span class="string">"已锁定数据包的化合物不能重复推进"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">stage</span> = <span class="string">"candidate"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">lockedDataPackageId</span> = input.<span class="property">dataPackageId</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">updatedByUserId</span> = input.<span class="property">actorUserId</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="title function_">returnToOptimization</span>(<span class="params"><span class="attr">input</span>: { reason: <span class="built_in">string</span>; actorUserId: <span class="built_in">string</span> }</span>) {</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">stage</span> !== <span class="string">"lead_optimization"</span>) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">DomainError</span>(<span class="string">"只有评审中的化合物才能退回优化"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">stage</span> = <span class="string">"lead_optimization"</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">optimizationReason</span> = input.<span class="property">reason</span>;</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">updatedByUserId</span> = input.<span class="property">actorUserId</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Unit of Work 在事务里调用这些方法:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> prisma.$transaction(<span class="title function_">async</span> (tx) => {</span><br><span class="line"> <span class="keyword">const</span> compoundStore = <span class="keyword">new</span> <span class="title class_">PrismaCompoundStore</span>(tx);</span><br><span class="line"> <span class="keyword">const</span> compound = <span class="keyword">await</span> compoundStore.<span class="title function_">findById</span>(input.<span class="property">compoundId</span>);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (input.<span class="property">result</span> === <span class="string">"promote"</span>) {</span><br><span class="line"> compound.<span class="title function_">promoteToCandidate</span>({</span><br><span class="line"> <span class="attr">dataPackageId</span>: input.<span class="property">dataPackageId</span>,</span><br><span class="line"> <span class="attr">actorUserId</span>: input.<span class="property">actorUserId</span>,</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (input.<span class="property">result</span> === <span class="string">"optimize"</span>) {</span><br><span class="line"> compound.<span class="title function_">returnToOptimization</span>({</span><br><span class="line"> <span class="attr">reason</span>: input.<span class="property">reason</span> ?? <span class="string">"需要补充结构优化"</span>,</span><br><span class="line"> <span class="attr">actorUserId</span>: input.<span class="property">actorUserId</span>,</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">await</span> compoundStore.<span class="title function_">save</span>(compound);</span><br><span class="line"> <span class="keyword">await</span> decisionStore.<span class="title function_">save</span>(decision);</span><br><span class="line"> <span class="keyword">await</span> candidateStore.<span class="title function_">createIfPromoted</span>(...);</span><br><span class="line"> <span class="keyword">await</span> assignmentStore.<span class="title function_">closeCurrent</span>(...);</span><br><span class="line"> <span class="keyword">await</span> auditStore.<span class="title function_">append</span>(...);</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
<p>而为了让事务里的代码复用聚合持久化逻辑,我们需要一种 tx-bound repository 或 store。</p>
<p>最粗暴的写法是在领域 repository interface 里加一个可选事务参数:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">interface</span> <span class="title class_">CompoundRepository</span> {</span><br><span class="line"> <span class="title function_">findById</span>(<span class="attr">id</span>: <span class="built_in">string</span>, <span class="attr">tx</span>?: <span class="title class_">Prisma</span>.<span class="property">TransactionClient</span>): <span class="title class_">Promise</span><<span class="title class_">Compound</span> | <span class="literal">null</span>>;</span><br><span class="line"> <span class="title function_">save</span>(<span class="attr">compound</span>: <span class="title class_">Compound</span>, <span class="attr">tx</span>?: <span class="title class_">Prisma</span>.<span class="property">TransactionClient</span>): <span class="title class_">Promise</span><<span class="built_in">void</span>>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>但它又把 Prisma 带进了领域层。就算把 <code>Prisma.TransactionClient</code> 包成 <code>TransactionClient</code> 类型别名,领域接口仍然在为基础设施妥协。</p>
<p>我更倾向于把 tx-bound 能力留在 infrastructure,领域层只定义干净的 repository port:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">export</span> <span class="keyword">interface</span> <span class="title class_">CompoundRepository</span> {</span><br><span class="line"> <span class="title function_">findById</span>(<span class="attr">id</span>: <span class="built_in">string</span>): <span class="title class_">Promise</span><<span class="title class_">Compound</span> | <span class="literal">null</span>>;</span><br><span class="line"> <span class="title function_">save</span>(<span class="attr">compound</span>: <span class="title class_">Compound</span>): <span class="title class_">Promise</span><<span class="built_in">void</span>>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>infrastructure 层拆一个 store,接收最小持久化客户端:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> <span class="title class_">CompoundStoreClient</span> = {</span><br><span class="line"> <span class="attr">compound</span>: {</span><br><span class="line"> <span class="title function_">findUnique</span>(<span class="attr">args</span>: <span class="built_in">unknown</span>): <span class="title class_">Promise</span><<span class="built_in">unknown</span>>;</span><br><span class="line"> <span class="title function_">update</span>(<span class="attr">args</span>: <span class="built_in">unknown</span>): <span class="title class_">Promise</span><<span class="built_in">unknown</span>>;</span><br><span class="line"> };</span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">PrismaCompoundStore</span> <span class="keyword">implements</span> <span class="title class_">CompoundRepository</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="attr">client</span>: <span class="title class_">CompoundStoreClient</span></span>) {}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">findById</span>(<span class="params"><span class="attr">id</span>: <span class="built_in">string</span></span>) {</span><br><span class="line"> <span class="keyword">const</span> record = <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">client</span>.<span class="property">compound</span>.<span class="title function_">findUnique</span>(...);</span><br><span class="line"> <span class="keyword">return</span> <span class="title class_">Compound</span>.<span class="title function_">restore</span>(record);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">async</span> <span class="title function_">save</span>(<span class="params"><span class="attr">compound</span>: <span class="title class_">Compound</span></span>) {</span><br><span class="line"> <span class="keyword">const</span> snapshot = compound.<span class="title function_">toSnapshot</span>();</span><br><span class="line"> <span class="keyword">await</span> <span class="variable language_">this</span>.<span class="property">client</span>.<span class="property">compound</span>.<span class="title function_">update</span>(...);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>普通 repository 可以继承这个 store,传完整 Prisma client:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">PrismaCompoundRepository</span> <span class="keyword">extends</span> <span class="title class_ inherited__">PrismaCompoundStore</span> {</span><br><span class="line"> <span class="title function_">constructor</span>(<span class="params"><span class="attr">prisma</span>: <span class="title class_">PrismaClient</span></span>) {</span><br><span class="line"> <span class="variable language_">super</span>(prisma);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>
<p>Unit of Work 在事务里传 tx:</p>
<figure class="highlight ts"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> prisma.$transaction(<span class="title function_">async</span> (tx) => {</span><br><span class="line"> <span class="keyword">const</span> compoundStore = <span class="keyword">new</span> <span class="title class_">PrismaCompoundStore</span>(tx);</span><br><span class="line"> <span class="keyword">const</span> compound = <span class="keyword">await</span> compoundStore.<span class="title function_">findById</span>(input.<span class="property">compoundId</span>);</span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line"> <span class="keyword">await</span> compoundStore.<span class="title function_">save</span>(compound);</span><br><span class="line">});</span><br></pre></td></tr></table></figure>
</div>
<script>
window.onload = detectors();
</script>
<div class="post-footer">
<div class="h-line-primary"></div>
<nav class="post-nav">
<div class="prev-item">
<div class="icon arrow-left"></div>
<div class="post-link">
<a href="/2026/08/15/%E8%A1%80%E6%B8%85%E8%83%86%E7%BA%A2%E7%B4%A0%E6%B0%B4%E5%B9%B3%E4%B8%8E%E5%86%A0%E5%BF%83%E7%97%85%E9%A3%8E%E9%99%A9/">Prev</a>
</div>
</div>
<div class="next-item">
<div class="icon arrow-right"></div>
<div class="post-link">
<a href="/2026/04/21/%E4%BA%8C%E3%80%87%E4%BA%8C%E5%85%AD%E5%B9%B4%E5%9B%9B%E6%9C%88%E4%BA%8C%E5%8D%81%E4%B8%80%E6%97%A5/">Next</a>
</div>
</div>
</nav>
</div>
<div class="post-comment">
</div>
</article>
</div>
</div>
<div class="footer">
<div class="flex-container">
<div class="footer-text">
韩暮秋 |
希望路过的人可以添点柴火让这里暖和点
</div>
</div>
</div>
</div>
<script src="/js/mermaid-zoom.js"></script>
</body>
</html>
|