summaryrefslogtreecommitdiff
path: root/2026/05/28/如何在-DDD-CQRS-中处理跨聚合的事务
diff options
context:
space:
mode:
Diffstat (limited to '2026/05/28/如何在-DDD-CQRS-中处理跨聚合的事务')
-rw-r--r--2026/05/28/如何在-DDD-CQRS-中处理跨聚合的事务/index.html303
1 files changed, 303 insertions, 0 deletions
diff --git a/2026/05/28/如何在-DDD-CQRS-中处理跨聚合的事务/index.html b/2026/05/28/如何在-DDD-CQRS-中处理跨聚合的事务/index.html
new file mode 100644
index 00000000..e7a633da
--- /dev/null
+++ b/2026/05/28/如何在-DDD-CQRS-中处理跨聚合的事务/index.html
@@ -0,0 +1,303 @@
+<!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> &#123;</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>) &#123;&#125;</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>) &#123;</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) =&gt; &#123;</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>(&#123;</span><br><span class="line"> <span class="attr">data</span>: &#123;</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"> &#125;,</span><br><span class="line"> &#125;);</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>(&#123;</span><br><span class="line"> <span class="attr">where</span>: &#123; <span class="attr">id</span>: command.<span class="property">compoundId</span> &#125;,</span><br><span class="line"> <span class="attr">data</span>: &#123; <span class="attr">stage</span>: command.<span class="property">result</span> === <span class="string">&quot;promote&quot;</span> ? <span class="string">&quot;candidate&quot;</span> : <span class="string">&quot;lead&quot;</span> &#125;,</span><br><span class="line"> &#125;);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (command.<span class="property">result</span> === <span class="string">&quot;promote&quot;</span>) &#123;</span><br><span class="line"> <span class="keyword">await</span> tx.<span class="property">developmentCandidate</span>.<span class="title function_">create</span>(&#123;</span><br><span class="line"> <span class="attr">data</span>: &#123; <span class="attr">compoundId</span>: command.<span class="property">compoundId</span> &#125;,</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125;</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"> &#125;);</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</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> &#123;</span><br><span class="line"> <span class="title function_">submit</span>(<span class="attr">input</span>: &#123;</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">&quot;promote&quot;</span> | <span class="string">&quot;optimize&quot;</span> | <span class="string">&quot;pause&quot;</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"> &#125;): <span class="title class_">Promise</span>&lt;<span class="title class_">CompoundPromotionDecisionDto</span>&gt;;</span><br><span class="line">&#125;</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> &#123;</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>) &#123;&#125;</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>) &#123;</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>(&#123;</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"> &#125;);</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</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">&#123;</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>) &#123;&#125;</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>) &#123;</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) =&gt; &#123;</span><br><span class="line"> <span class="comment">// 在这里组合 tx-bound store / repository</span></span><br><span class="line"> &#125;);</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</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> &#123;</span><br><span class="line"> <span class="title function_">promoteToCandidate</span>(<span class="params"><span class="attr">input</span>: &#123;</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"> &#125;</span>) &#123;</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">stage</span> !== <span class="string">&quot;lead_optimization&quot;</span>) &#123;</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">DomainError</span>(<span class="string">&quot;只有先导优化阶段的化合物才能推进为候选化合物&quot;</span>);</span><br><span class="line"> &#125;</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>) &#123;</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">DomainError</span>(<span class="string">&quot;已锁定数据包的化合物不能重复推进&quot;</span>);</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">stage</span> = <span class="string">&quot;candidate&quot;</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"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="title function_">returnToOptimization</span>(<span class="params"><span class="attr">input</span>: &#123; reason: <span class="built_in">string</span>; actorUserId: <span class="built_in">string</span> &#125;</span>) &#123;</span><br><span class="line"> <span class="keyword">if</span> (<span class="variable language_">this</span>.<span class="property">stage</span> !== <span class="string">&quot;lead_optimization&quot;</span>) &#123;</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">DomainError</span>(<span class="string">&quot;只有评审中的化合物才能退回优化&quot;</span>);</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">stage</span> = <span class="string">&quot;lead_optimization&quot;</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"> &#125;</span><br><span class="line">&#125;</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) =&gt; &#123;</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">&quot;promote&quot;</span>) &#123;</span><br><span class="line"> compound.<span class="title function_">promoteToCandidate</span>(&#123;</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"> &#125;);</span><br><span class="line"> &#125;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (input.<span class="property">result</span> === <span class="string">&quot;optimize&quot;</span>) &#123;</span><br><span class="line"> compound.<span class="title function_">returnToOptimization</span>(&#123;</span><br><span class="line"> <span class="attr">reason</span>: input.<span class="property">reason</span> ?? <span class="string">&quot;需要补充结构优化&quot;</span>,</span><br><span class="line"> <span class="attr">actorUserId</span>: input.<span class="property">actorUserId</span>,</span><br><span class="line"> &#125;);</span><br><span class="line"> &#125;</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">&#125;);</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> &#123;</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>&lt;<span class="title class_">Compound</span> | <span class="literal">null</span>&gt;;</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>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">&#125;</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> &#123;</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>&lt;<span class="title class_">Compound</span> | <span class="literal">null</span>&gt;;</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>&lt;<span class="built_in">void</span>&gt;;</span><br><span class="line">&#125;</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> = &#123;</span><br><span class="line"> <span class="attr">compound</span>: &#123;</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>&lt;<span class="built_in">unknown</span>&gt;;</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>&lt;<span class="built_in">unknown</span>&gt;;</span><br><span class="line"> &#125;;</span><br><span class="line">&#125;;</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> &#123;</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>) &#123;&#125;</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>) &#123;</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"> &#125;</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>) &#123;</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"> &#125;</span><br><span class="line">&#125;</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> &#123;</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>) &#123;</span><br><span class="line"> <span class="variable language_">super</span>(prisma);</span><br><span class="line"> &#125;</span><br><span class="line">&#125;</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) =&gt; &#123;</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">&#125;);</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>
+ <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>