summaryrefslogtreecommitdiff
path: root/search.xml
diff options
context:
space:
mode:
Diffstat (limited to 'search.xml')
-rw-r--r--search.xml91
1 files changed, 77 insertions, 14 deletions
diff --git a/search.xml b/search.xml
index b7441f2b..661b696b 100644
--- a/search.xml
+++ b/search.xml
@@ -3431,20 +3431,6 @@ NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的�
</tags>
</entry>
<entry>
- <title>使用 [@poll error] 实现线程安全的数据结构</title>
- <url>/2023/08/15/poll-error-attribute-in-OCaml/</url>
- <content><![CDATA[<p>OCaml的标准库提供了许多mutable的数据结构,比如Hashtbl, Queue, Stack之类的,但是这些数据结构都不是线程安全的。在 OCaml 4 和 OCaml 5 中,单个Domain中一次只能运行一个线程。换句话说,单个Domain中的线程仍然不会并行运行,除非在不同所的Domain中。</p>
-<p>而在单个Domain中 OCaml 的 runtime 是通过在safe point期间半抢占式的切换线程。也就是说,线程切换只会发生在safe point期间。例如内存分配就是是safe point。这意味着在没有safe point的代码块内,可以在Domain内原子性地进行多次读写或访问操作,因为线程没被切换。</p>
-<p>OCaml 编译器提供了一个名为 <a href="https://github.com/ocaml/ocaml/pull/10462"><code>[@poll error]</code></a> 的annotation,可以在函数中使用它来确保该函数不包含safe point。</p>
-<p>所以通过使用 <code>[@poll error]</code> 就可以创建在Domain内原子性执行的函数,也就是说,基于此特性可以实现单个Domain内线程安全的数据结构,例如 thread-table 便是使用这个特性实现的 Hash Table。可以看看它的 add 函数的实现:</p>
-<figure class="highlight ocaml"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span>[@poll error] add_atomically t buckets n i before after =</span><br><span class="line"> t.rehash = <span class="number">0</span> &amp;&amp; buckets == t.buckets</span><br><span class="line"> &amp;&amp; before == <span class="type">Array</span>.unsafe_get buckets i</span><br><span class="line"> &amp;&amp; <span class="keyword">begin</span></span><br><span class="line"> <span class="type">Array</span>.unsafe_set buckets i after;</span><br><span class="line"> <span class="keyword">let</span> length = t.length + <span class="number">1</span> <span class="keyword">in</span></span><br><span class="line"> t.length &lt;- length;</span><br><span class="line"> <span class="keyword">if</span> n &lt; length &amp;&amp; n &lt; max_buckets_div_2 <span class="keyword">then</span> t.rehash &lt;- n * <span class="number">2</span>;</span><br><span class="line"> <span class="literal">true</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> <span class="keyword">rec</span> add t k&#x27; v&#x27; =</span><br><span class="line"> <span class="keyword">let</span> h = <span class="type">Mix</span>.<span class="built_in">int</span> k&#x27; <span class="keyword">in</span></span><br><span class="line"> maybe_rehash t;</span><br><span class="line"> <span class="keyword">let</span> buckets = t.buckets <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> n = <span class="type">Array</span>.length buckets <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> i = h <span class="keyword">land</span> (n - <span class="number">1</span>) <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> before = <span class="type">Array</span>.unsafe_get buckets i <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> after = <span class="type">Cons</span> (k&#x27;, v&#x27;, before) <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">if</span> not (add_atomically t buckets n i before after) <span class="keyword">then</span> add t k&#x27; v&#x27;</span><br></pre></td></tr></table></figure>
-<p>相比使用 Stdlib.Mutex,这种无锁实现会有更好的性能(特别是对于只读操作),并且还允许例如信号处理之类的上下文操作。</p>
-]]></content>
- <tags>
- <tag>Technique</tag>
- </tags>
- </entry>
- <entry>
<title>除权除息日,股价调整与分红</title>
<url>/2025/07/11/rep-date/</url>
<content><![CDATA[<p>例如:</p>
@@ -3492,6 +3478,20 @@ NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的�
</tags>
</entry>
<entry>
+ <title>使用 [@poll error] 实现线程安全的数据结构</title>
+ <url>/2023/08/15/poll-error-attribute-in-OCaml/</url>
+ <content><![CDATA[<p>OCaml的标准库提供了许多mutable的数据结构,比如Hashtbl, Queue, Stack之类的,但是这些数据结构都不是线程安全的。在 OCaml 4 和 OCaml 5 中,单个Domain中一次只能运行一个线程。换句话说,单个Domain中的线程仍然不会并行运行,除非在不同所的Domain中。</p>
+<p>而在单个Domain中 OCaml 的 runtime 是通过在safe point期间半抢占式的切换线程。也就是说,线程切换只会发生在safe point期间。例如内存分配就是是safe point。这意味着在没有safe point的代码块内,可以在Domain内原子性地进行多次读写或访问操作,因为线程没被切换。</p>
+<p>OCaml 编译器提供了一个名为 <a href="https://github.com/ocaml/ocaml/pull/10462"><code>[@poll error]</code></a> 的annotation,可以在函数中使用它来确保该函数不包含safe point。</p>
+<p>所以通过使用 <code>[@poll error]</code> 就可以创建在Domain内原子性执行的函数,也就是说,基于此特性可以实现单个Domain内线程安全的数据结构,例如 thread-table 便是使用这个特性实现的 Hash Table。可以看看它的 add 函数的实现:</p>
+<figure class="highlight ocaml"><table><tr><td class="code"><pre><span class="line"><span class="keyword">let</span>[@poll error] add_atomically t buckets n i before after =</span><br><span class="line"> t.rehash = <span class="number">0</span> &amp;&amp; buckets == t.buckets</span><br><span class="line"> &amp;&amp; before == <span class="type">Array</span>.unsafe_get buckets i</span><br><span class="line"> &amp;&amp; <span class="keyword">begin</span></span><br><span class="line"> <span class="type">Array</span>.unsafe_set buckets i after;</span><br><span class="line"> <span class="keyword">let</span> length = t.length + <span class="number">1</span> <span class="keyword">in</span></span><br><span class="line"> t.length &lt;- length;</span><br><span class="line"> <span class="keyword">if</span> n &lt; length &amp;&amp; n &lt; max_buckets_div_2 <span class="keyword">then</span> t.rehash &lt;- n * <span class="number">2</span>;</span><br><span class="line"> <span class="literal">true</span></span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">let</span> <span class="keyword">rec</span> add t k&#x27; v&#x27; =</span><br><span class="line"> <span class="keyword">let</span> h = <span class="type">Mix</span>.<span class="built_in">int</span> k&#x27; <span class="keyword">in</span></span><br><span class="line"> maybe_rehash t;</span><br><span class="line"> <span class="keyword">let</span> buckets = t.buckets <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> n = <span class="type">Array</span>.length buckets <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> i = h <span class="keyword">land</span> (n - <span class="number">1</span>) <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> before = <span class="type">Array</span>.unsafe_get buckets i <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">let</span> after = <span class="type">Cons</span> (k&#x27;, v&#x27;, before) <span class="keyword">in</span></span><br><span class="line"> <span class="keyword">if</span> not (add_atomically t buckets n i before after) <span class="keyword">then</span> add t k&#x27; v&#x27;</span><br></pre></td></tr></table></figure>
+<p>相比使用 Stdlib.Mutex,这种无锁实现会有更好的性能(特别是对于只读操作),并且还允许例如信号处理之类的上下文操作。</p>
+]]></content>
+ <tags>
+ <tag>Technique</tag>
+ </tags>
+ </entry>
+ <entry>
<title>shadow-cljs 怎么设置代理</title>
<url>/2024/04/03/shadow-cljs-proxy/</url>
<content><![CDATA[<p>The library pomegranate used for retrieving maven dependencies does not read <code>.m2/settings.xml</code>. You can however set the :proxy config either in <code>shadow-cljs.edn</code> directly or <code>~/.shadow-cljs/config.edn</code>.</p>
@@ -5398,6 +5398,69 @@ NestJS 依赖其底层 HTTP 适配器(默认为 Express)来解析传入的�
</tags>
</entry>
<entry>
+ <title>如何在 DDD + CQRS 中处理跨聚合的事务</title>
+ <url>/2026/05/28/%E5%A6%82%E4%BD%95%E5%9C%A8-DDD-CQRS-%E4%B8%AD%E5%A4%84%E7%90%86%E8%B7%A8%E8%81%9A%E5%90%88%E7%9A%84%E4%BA%8B%E5%8A%A1/</url>
+ <content><![CDATA[<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="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="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="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="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="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="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="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="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="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="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="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="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="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>
+]]></content>
+ <tags>
+ <tag>Technique</tag>
+ </tags>
+ </entry>
+ <entry>
<title>如何管理用户界面中的危险操作</title>
<url>/2024/11/26/%E5%A6%82%E4%BD%95%E7%AE%A1%E7%90%86%E7%94%A8%E6%88%B7%E7%95%8C%E9%9D%A2%E4%B8%AD%E7%9A%84%E5%8D%B1%E9%99%A9%E6%93%8D%E4%BD%9C/</url>
<content><![CDATA[<blockquote>