<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Repository-Adapter on Tarragon</title><link>https://tarrragon.github.io/blog/tags/repository-adapter/</link><description>Recent content in Repository-Adapter on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 13 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/repository-adapter/index.xml" rel="self" type="application/rss+xml"/><item><title>1.4 Repository Adapter 實作</title><link>https://tarrragon.github.io/blog/backend/01-database/repository-adapter/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/repository-adapter/</guid><description>&lt;p>資料庫倉儲轉接層（repository adapter）的核心責任是把應用層語意轉成資料庫可執行操作、並把資料庫錯誤回譯成業務可判讀結果。它是 &lt;code>domain model&lt;/code> 和 &lt;code>SQL model&lt;/code> 之間的邊界層、不承擔業務流程編排。&lt;/p>
&lt;p>本章從 hexagonal architecture 的 port / adapter 模式出發、處理 mapping、error translation、testing 跟跨服務 transaction 等實作議題。讀完後讀者能設計一個可演進、可測試、可換 DB 的 repository 層。&lt;/p>
&lt;h2 id="port--adapter-邊界">Port / Adapter 邊界&lt;/h2>
&lt;p>Repository 在 hexagonal architecture（也叫 ports &amp;amp; adapters）中是 &lt;em>outbound port&lt;/em> 的實作。&lt;/p>
&lt;p>&lt;strong>Port（domain layer 定義）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>抽象 interface / protocol、描述 &lt;em>領域語意&lt;/em>&lt;/li>
&lt;li>不暴露 SQL、不暴露 DB 細節&lt;/li>
&lt;li>例：&lt;code>type OrderRepository interface { Find(id) Order; Save(order); ... }&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Adapter（infrastructure layer 實作）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>實作 port、負責跟具體 DB 對話&lt;/li>
&lt;li>翻譯 domain entity ↔ DB row&lt;/li>
&lt;li>翻譯 DB error → domain error&lt;/li>
&lt;li>例：&lt;code>type SQLOrderRepository struct { db *sql.DB }&lt;/code>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>為什麼這層抽象有價值&lt;/strong>：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>可替換性&lt;/strong>：DB 換 vendor 時、domain layer 不必改&lt;/li>
&lt;li>&lt;strong>可測試性&lt;/strong>：在 domain layer test 時可注入 memory fake、不必起 DB&lt;/li>
&lt;li>&lt;strong>語意清楚&lt;/strong>：domain 不被 SQL 細節污染、business rule 集中&lt;/li>
&lt;li>&lt;strong>演進可控&lt;/strong>：schema 改動時、只在 adapter 改 mapping、不擴散到全程式&lt;/li>
&lt;/ol>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter 卡片&lt;/a>。&lt;/p>
&lt;h2 id="adapter-三個核心責任">Adapter 三個核心責任&lt;/h2>
&lt;p>adapter 接收應用層輸入、負責三件事：查詢與命令組裝、row mapping、錯誤翻譯。業務規則判斷留在 service / usecase 層、adapter 聚焦在資料持久化語意與資料庫行為。&lt;/p>
&lt;p>邊界清楚的好處是演進可控。schema 調整時、只需要在 adapter 收斂欄位映射與查詢變更、不用把 SQL 細節滲透回 domain 層。&lt;/p>
&lt;h3 id="1-查詢與命令組裝">1. 查詢與命令組裝&lt;/h3>
&lt;p>把 domain 操作翻成具體 SQL / NoSQL query。實作層級有取捨：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Raw SQL&lt;/strong>：完全控制、易追 query plan、但容易拼錯字、易 SQL injection&lt;/li>
&lt;li>&lt;strong>Query builder&lt;/strong>（GORM Build、Knex、SQLAlchemy Core）：型別安全、不寫字串、但學 DSL&lt;/li>
&lt;li>&lt;strong>ORM&lt;/strong>（GORM、SQLAlchemy ORM、Active Record）：高抽象、自動 mapping、但隱藏細節、容易產生 N+1&lt;/li>
&lt;/ul>
&lt;p>詳見下方「ORM vs Query Builder vs Raw SQL」段。&lt;/p>
&lt;h3 id="2-row-mapping-與-nullable-handling">2. Row Mapping 與 Nullable Handling&lt;/h3>
&lt;p>row mapping 的責任是把資料庫欄位轉成穩定模型。欄位型別、時間格式、枚舉值、可空欄位都要有明確轉換規則。可空欄位需要顯式處理、避免把「缺值」誤當有效預設值。&lt;/p>
&lt;p>&lt;strong>Nullable handling 模式&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Optional type&lt;/strong>：Go &lt;code>sql.NullString&lt;/code>、Java &lt;code>Optional&amp;lt;T&amp;gt;&lt;/code>、Rust &lt;code>Option&amp;lt;T&amp;gt;&lt;/code>、Python &lt;code>Optional[T]&lt;/code>&lt;/li>
&lt;li>&lt;strong>Sentinel value&lt;/strong>：用特殊值代表 null（不推薦、易混淆）&lt;/li>
&lt;li>&lt;strong>Default fallback&lt;/strong>：null → 預設值（要明確、不要悄悄轉換）&lt;/li>
&lt;/ul>
&lt;p>資料模型演進時、新舊欄位可能共存。adapter 要支援過渡期讀寫相容、讓版本切換能分批進行。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>資料庫倉儲轉接層（repository adapter）的核心責任是把應用層語意轉成資料庫可執行操作、並把資料庫錯誤回譯成業務可判讀結果。它是 <code>domain model</code> 和 <code>SQL model</code> 之間的邊界層、不承擔業務流程編排。</p>
<p>本章從 hexagonal architecture 的 port / adapter 模式出發、處理 mapping、error translation、testing 跟跨服務 transaction 等實作議題。讀完後讀者能設計一個可演進、可測試、可換 DB 的 repository 層。</p>
<h2 id="port--adapter-邊界">Port / Adapter 邊界</h2>
<p>Repository 在 hexagonal architecture（也叫 ports &amp; adapters）中是 <em>outbound port</em> 的實作。</p>
<p><strong>Port（domain layer 定義）</strong>：</p>
<ul>
<li>抽象 interface / protocol、描述 <em>領域語意</em></li>
<li>不暴露 SQL、不暴露 DB 細節</li>
<li>例：<code>type OrderRepository interface { Find(id) Order; Save(order); ... }</code></li>
</ul>
<p><strong>Adapter（infrastructure layer 實作）</strong>：</p>
<ul>
<li>實作 port、負責跟具體 DB 對話</li>
<li>翻譯 domain entity ↔ DB row</li>
<li>翻譯 DB error → domain error</li>
<li>例：<code>type SQLOrderRepository struct { db *sql.DB }</code></li>
</ul>
<p><strong>為什麼這層抽象有價值</strong>：</p>
<ol>
<li><strong>可替換性</strong>：DB 換 vendor 時、domain layer 不必改</li>
<li><strong>可測試性</strong>：在 domain layer test 時可注入 memory fake、不必起 DB</li>
<li><strong>語意清楚</strong>：domain 不被 SQL 細節污染、business rule 集中</li>
<li><strong>演進可控</strong>：schema 改動時、只在 adapter 改 mapping、不擴散到全程式</li>
</ol>
<p>詳見 <a href="/blog/backend/knowledge-cards/repository-adapter/" data-link-title="Repository Adapter" data-link-desc="說明持久化層如何把資料模型轉成外部儲存介面">Repository Adapter 卡片</a>。</p>
<h2 id="adapter-三個核心責任">Adapter 三個核心責任</h2>
<p>adapter 接收應用層輸入、負責三件事：查詢與命令組裝、row mapping、錯誤翻譯。業務規則判斷留在 service / usecase 層、adapter 聚焦在資料持久化語意與資料庫行為。</p>
<p>邊界清楚的好處是演進可控。schema 調整時、只需要在 adapter 收斂欄位映射與查詢變更、不用把 SQL 細節滲透回 domain 層。</p>
<h3 id="1-查詢與命令組裝">1. 查詢與命令組裝</h3>
<p>把 domain 操作翻成具體 SQL / NoSQL query。實作層級有取捨：</p>
<ul>
<li><strong>Raw SQL</strong>：完全控制、易追 query plan、但容易拼錯字、易 SQL injection</li>
<li><strong>Query builder</strong>（GORM Build、Knex、SQLAlchemy Core）：型別安全、不寫字串、但學 DSL</li>
<li><strong>ORM</strong>（GORM、SQLAlchemy ORM、Active Record）：高抽象、自動 mapping、但隱藏細節、容易產生 N+1</li>
</ul>
<p>詳見下方「ORM vs Query Builder vs Raw SQL」段。</p>
<h3 id="2-row-mapping-與-nullable-handling">2. Row Mapping 與 Nullable Handling</h3>
<p>row mapping 的責任是把資料庫欄位轉成穩定模型。欄位型別、時間格式、枚舉值、可空欄位都要有明確轉換規則。可空欄位需要顯式處理、避免把「缺值」誤當有效預設值。</p>
<p><strong>Nullable handling 模式</strong>：</p>
<ul>
<li><strong>Optional type</strong>：Go <code>sql.NullString</code>、Java <code>Optional&lt;T&gt;</code>、Rust <code>Option&lt;T&gt;</code>、Python <code>Optional[T]</code></li>
<li><strong>Sentinel value</strong>：用特殊值代表 null（不推薦、易混淆）</li>
<li><strong>Default fallback</strong>：null → 預設值（要明確、不要悄悄轉換）</li>
</ul>
<p>資料模型演進時、新舊欄位可能共存。adapter 要支援過渡期讀寫相容、讓版本切換能分批進行。詳見 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a>。</p>
<h3 id="3-error-translation">3. Error Translation</h3>
<p>error translation 的責任是把底層錯誤分類成應用層可決策訊號。唯一鍵衝突、外鍵限制、交易衝突、連線逾時、都需要翻譯成可行動錯誤類型、而不是將原生錯誤字串直接外漏。</p>
<p><strong>常見錯誤分類</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Domain error</th>
          <th>SQL error 對應</th>
          <th>應用層動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>ErrAlreadyExists</code></td>
          <td><code>unique_violation</code>（PostgreSQL 23505）</td>
          <td>409 Conflict / 業務 retry</td>
      </tr>
      <tr>
          <td><code>ErrNotFound</code></td>
          <td>empty result set</td>
          <td>404</td>
      </tr>
      <tr>
          <td><code>ErrConstraintFailed</code></td>
          <td><code>foreign_key_violation</code>（23503）</td>
          <td>400 Bad Request</td>
      </tr>
      <tr>
          <td><code>ErrConflict</code></td>
          <td><code>serialization_failure</code>（40001）</td>
          <td>retry with backoff</td>
      </tr>
      <tr>
          <td><code>ErrTimeout</code></td>
          <td><code>query_canceled</code>（57014）/ context deadline</td>
          <td>retry / circuit break</td>
      </tr>
      <tr>
          <td><code>ErrUnavailable</code></td>
          <td>connection refused / pool exhausted</td>
          <td>circuit break / fallback</td>
      </tr>
  </tbody>
</table>
<p>這層翻譯會直接影響重試、回退與事故判讀。分類越穩定、越能在 06/08 模組形成一致決策語言。</p>
<h2 id="orm-vs-query-builder-vs-raw-sql">ORM vs Query Builder vs Raw SQL</h2>
<p>選 mapping 工具是 repository adapter 的核心取捨。</p>
<h3 id="raw-sql">Raw SQL</h3>
<ul>
<li>優勢：完全控制 query plan、易 tune</li>
<li>優勢：大規模 query 性能最好</li>
<li>限制：易拼錯字、IDE 支援差</li>
<li>風險：一不小心就 SQL injection（用 prepared statement / parameterized query）</li>
<li>適合：性能極限關鍵 / 複雜 query / 已有 SQL 專家團隊</li>
</ul>
<h3 id="query-builder">Query Builder</h3>
<p>主流工具：Knex（Node）、SQLAlchemy Core（Python）、jOOQ（Java）、sqlc（Go）、Diesel（Rust）。</p>
<ul>
<li>優勢：型別安全、IDE 自動完成</li>
<li>優勢：不需要 ORM 的複雜度</li>
<li>優勢：仍可看到生成的 SQL</li>
<li>限制：學 DSL 成本</li>
<li>適合：中等複雜度 + 想要安全性 + 想看 SQL</li>
</ul>
<h3 id="orm">ORM</h3>
<p>主流工具：GORM（Go）、SQLAlchemy ORM（Python）、Active Record（Rails）、JPA / Hibernate（Java）、Entity Framework（.NET）、Prisma（TypeScript）。</p>
<ul>
<li>優勢：CRUD 操作快速、boilerplate 少</li>
<li>優勢：自動 mapping、自動 transaction</li>
<li>優勢：migration 工具通常整合</li>
<li>限制：隱藏 SQL 細節、易產生 N+1 query</li>
<li>限制：複雜 query 反而比 raw SQL 難寫</li>
<li>風險：lazy loading 容易意外性能問題</li>
<li>適合：CRUD 為主的應用、團隊偏業務開發</li>
</ul>
<h3 id="選型決策">選型決策</h3>
<ol>
<li><strong>小團隊 + CRUD-heavy</strong>：ORM（快速 prototype、boilerplate 少）</li>
<li><strong>中型 + 混合需求</strong>：Query Builder（安全 + 仍能寫複雜 query）</li>
<li><strong>大型 + 性能極限</strong>：Raw SQL + Query Builder（複雜 query 用 raw、簡單用 builder）</li>
<li><strong>microservice 私有 store</strong>：通常 Query Builder 為主（見 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a> 模式）</li>
</ol>
<h3 id="orm-反模式">ORM 反模式</h3>
<ul>
<li><code>find()</code> 隨手呼叫導致 N+1 query</li>
<li>lazy loading 在 view 層觸發 query</li>
<li>用 ORM 寫複雜 aggregation（應該 raw SQL）</li>
<li>不 eager load 關聯資料</li>
</ul>
<h2 id="testing-策略">Testing 策略</h2>
<p>repository 是 <em>infrastructure</em> 層、test 策略不同於 domain layer。</p>
<h3 id="memory-fakeunit-test-友善">Memory Fake（unit test 友善）</h3>
<ul>
<li>用 in-memory implementation 滿足 port interface</li>
<li>不必起 DB、快、可隔離</li>
<li>適合：domain layer test、test repository 的 <em>呼叫者</em></li>
<li>反模式：用 memory fake test repository 本身（測不到實際 SQL 行為）</li>
</ul>
<h3 id="integration-test驗證真實-db-行為">Integration Test（驗證真實 DB 行為）</h3>
<ul>
<li>用 testcontainers / Docker 起真實 DB（PostgreSQL / MySQL）</li>
<li>跑真實 SQL、抓真實 error</li>
<li>用 transaction rollback 隔離各 test</li>
<li>適合：test repository adapter 本身</li>
</ul>
<h3 id="contract-test">Contract Test</h3>
<ul>
<li>驗證 adapter 對外語意穩定：同一輸入是否得到一致輸出、同一錯誤是否被穩定分類、同一查詢語意在 schema 演進後是否保持相容</li>
<li>測試重點是邊界語意覆蓋、資料庫產品特性覆蓋是另一件事</li>
<li>例：「unique 衝突必須回 <code>ErrAlreadyExists</code>」這條 contract、不管底層是 PostgreSQL / MySQL / SQLite 都成立</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">Contract 卡片</a> 跟 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing</a>。</p>
<h3 id="sqlite-作為-test-db">SQLite 作為 test DB</h3>
<ul>
<li>起 quick、無 external dependency</li>
<li>但 SQL dialect 跟 PostgreSQL / MySQL 有差異</li>
<li>適合：簡單 query 的 test、不適合 production-fidelity test</li>
<li>對應 <a href="/blog/backend/01-database/vendors/sqlite/" data-link-title="SQLite" data-link-desc="embedded、單檔案、test / CLI / edge 場景的標準選擇、近年因 Cloudflare D1 / Turso 等服務復興">SQLite vendor page</a></li>
</ul>
<h2 id="transaction-傳遞">Transaction 傳遞</h2>
<p>repository 操作通常要支援「我自己起 transaction」跟「在已有 transaction 內操作」兩種模式。</p>
<p><strong>Pattern 1：repository 自己起 transaction</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">r</span> <span class="o">*</span><span class="nx">OrderRepo</span><span class="p">)</span> <span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">order</span> <span class="nx">Order</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="nx">tx</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">r</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">BeginTx</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kc">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">defer</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1">// ... 操作 ...</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>問題：跨多個 repository 時無法共用 transaction。</p>
<p><strong>Pattern 2：unit of work pattern</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">func</span> <span class="p">(</span><span class="nx">s</span> <span class="o">*</span><span class="nx">Service</span><span class="p">)</span> <span class="nf">PlaceOrder</span><span class="p">(</span><span class="nx">ctx</span> <span class="nx">context</span><span class="p">.</span><span class="nx">Context</span><span class="p">,</span> <span class="nx">order</span> <span class="nx">Order</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="nx">s</span><span class="p">.</span><span class="nx">uow</span><span class="p">.</span><span class="nf">Do</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="nx">Transaction</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">orderRepo</span><span class="p">.</span><span class="nf">Save</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">inventoryRepo</span><span class="p">.</span><span class="nf">Decrease</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">.</span><span class="nx">Items</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="nx">s</span><span class="p">.</span><span class="nx">paymentRepo</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="nx">tx</span><span class="p">,</span> <span class="nx">order</span><span class="p">.</span><span class="nx">Payment</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="k">return</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>把 transaction 從 repository 抽到 unit-of-work、跨 repository 共用。</p>
<p><strong>Pattern 3：context-based transaction</strong>：</p>
<ul>
<li>把 transaction 塞進 context</li>
<li>repository 從 context 拿 transaction（有 → 用、沒有 → 自己起）</li>
<li>Go 常用 pattern、但有「context 不該裝這種東西」的爭議</li>
</ul>
<p><strong>選擇邏輯</strong>：</p>
<ul>
<li>簡單應用：pattern 1 夠用</li>
<li>跨 repository transaction：pattern 2 或 3</li>
<li>大型 application：pattern 2（最清楚）</li>
</ul>
<p>詳見 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a>。</p>
<h2 id="microservice-私有-store-對應">Microservice 私有 Store 對應</h2>
<p>現代 microservice 設計強調「每個 service 私有 DB」、不跟其他 service 共用。</p>
<p><strong>對 repository adapter 的影響</strong>：</p>
<ul>
<li>每個 service 自己的 schema、自己的 adapter</li>
<li>跨 service 不直接 DB query、要透過 API</li>
<li>transaction 不跨 service（用 Saga 或 outbox）</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a>、<a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 100+ microservice</a></li>
</ul>
<p><strong>反模式</strong>：</p>
<ul>
<li>共用 DB schema、不同 service 都 query 同一張表 → 強耦合、schema 改一個影響全部</li>
<li>跨 service 用 DB foreign key → 不能 enforce、會壞掉</li>
</ul>
<h2 id="repository-adapter-五個常見變體">Repository Adapter 五個常見變體</h2>
<p>實務上 repository 不止「CRUD」這個樣態：</p>
<ol>
<li><strong>Pure CRUD repository</strong>：Find / Save / Delete、最簡單</li>
<li><strong>Aggregate repository</strong>：操作 aggregate root、含 nested entities</li>
<li><strong>Read model repository</strong>（CQRS）：專門 read、不 write</li>
<li><strong>Event-sourced repository</strong>：存 events、不存 state</li>
<li><strong>Cached repository</strong>：包一層 cache（pass-through、refresh-ahead）</li>
</ol>
<p>實作時要明確選哪種、不要讓一個 repository 跨多種 pattern。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一業務錯誤在不同路徑返回不同型別</td>
          <td>error translation 分類漂移</td>
          <td>收斂錯誤分類介面與 mapping</td>
      </tr>
      <tr>
          <td>schema 變更後應用層出現大量 null 問題</td>
          <td>nullable handling 規則不足</td>
          <td>補顯式轉換與 fallback 規則</td>
      </tr>
      <tr>
          <td>SQL 細節在 service 層大量出現</td>
          <td>adapter 邊界被繞過</td>
          <td>收斂資料操作入口到 repository</td>
      </tr>
      <tr>
          <td>同一查詢在不同環境結果不一致</td>
          <td>contract test 覆蓋不足</td>
          <td>補跨環境合約測試與 fixture</td>
      </tr>
      <tr>
          <td>事故排查時難以判斷重試與回退條件</td>
          <td>錯誤分類無法對應決策</td>
          <td>建立錯誤分類到 gate/incident 的映射表</td>
      </tr>
      <tr>
          <td>N+1 query 在 ORM 環境下出現</td>
          <td>lazy loading 反模式</td>
          <td>改 eager loading 或換 query builder</td>
      </tr>
      <tr>
          <td>跨 repository 的 transaction 不一致</td>
          <td>transaction 沒共用機制</td>
          <td>引入 unit-of-work pattern</td>
      </tr>
      <tr>
          <td>Test 跑很慢、需要起 DB</td>
          <td>test 沒分層</td>
          <td>unit test 用 memory fake、integration 才用 DB</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 repository adapter 寫成「直接包 SQL 的工具函式」、容易讓業務規則與資料邏輯混雜。邊界失焦後、schema 演進與事故修復都會擴大影響面。</p>
<p>把資料庫錯誤原樣往上拋、也會讓上層決策不穩定。錯誤翻譯是可靠性控制面的必要前置。</p>
<p>把 ORM 當銀彈、忘了 SQL 還在背後。N+1 query、lazy loading 災難、複雜 aggregation 反而難寫 — 這些都是「過度信任 ORM 抽象」的後果。</p>
<p>把 memory fake 拿來 test repository 本身、不會抓到實際 DB bug。memory fake 是給 <em>呼叫者</em> test 用的、不是給 repository test 用的。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>repository / adapter 設計重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix Aurora consolidation</a></td>
          <td>microservice 私有 store、每個 service 自己 repository</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft 100+ microservice</a></td>
          <td>微服務私有 DB、跨 service 不直接 DB query</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>TiDB → DynamoDB、repository adapter 是換 DB 的關鍵抽象</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>adapter 邊界可用 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 反例</a> 的資料一致性段落回寫。若事件中出現同一錯誤在不同路徑被不同方式處理、通常代表 adapter 的錯誤翻譯與契約分層不足。</p>
<p>這個案例主要支撐的是「錯誤分類與契約映射」判讀、不直接支撐 broker delivery 參數調整；若根因在 ack/retry 節奏、應回到 3.1/3.2。</p>
<p>回寫步驟是先盤點錯誤分類、再對齊重試與回退決策、最後把分類結果映射到 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing 與 Schema 演進</a> 的驗證欄位、讓發版前可先發現漂移。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.2 的交接：欄位與索引語意回到 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">schema design 與資料建模</a>。</li>
<li>與 1.3 的交接：交易錯誤與重試語意回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction 與一致性邊界</a>。</li>
<li>與 1.12 的交接：cross-DB migration 時、repository 是 <em>關鍵抽象</em> — 詳見 <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">大規模 DB 遷移實戰</a>。</li>
<li>與 6.10 的交接：跨服務契約一致性回到 <a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">Contract Testing 與 Schema 演進</a>。</li>
<li>與 8.19 的交接：資料層錯誤判斷與回退決策回到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行：<a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2 Schema Design</a>、<a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 Transaction Boundary</a></li>
<li>下游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 Database Migration Playbook</a> / <a href="/blog/backend/01-database/large-scale-db-migration/" data-link-title="1.12 大規模 DB 遷移實戰" data-link-desc="跨 DB 遷移的 dual-write、[shadow read](/backend/knowledge-cards/shadow-read/)、cutover、rollback 流程 — 從實戰案例提煉的工程做法">1.12 大規模 DB 遷移實戰</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/contract-testing/" data-link-title="6.10 Contract Testing 與 Schema 演進" data-link-desc="把跨服務 / API / event schema 的隱性期待變成可驗證契約，控制演進相容性">6.10 Contract Testing 與 Schema 演進</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>跨 vendor adapter 深入：<a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">DynamoDB single-table design</a>（document KV adapter 邊界）、<a href="/blog/backend/01-database/vendors/mongodb/schema-design-pattern/" data-link-title="MongoDB Schema Design Pattern：contract layer 在哪 vs embedded / reference" data-link-desc="MongoDB document schema 真正的 production 議題不是 embedded vs reference 二選一、是 schema contract 該放 DB 層 validator 還是 app 層 abstraction；含 Toyota polymorphic governance、Forbes abstraction layer、time-series collection 邊界">MongoDB schema design pattern</a>（document adapter 的 ODM 取捨）、<a href="/blog/backend/01-database/vendors/cosmosdb/mongodb-api-vs-sql-api/" data-link-title="Cosmos DB MongoDB API vs SQL API：遷移路徑、dogfood signal、multi-model、跨雲 hedging" data-link-desc="從『MongoDB API 跟 SQL API 哪個快』推進到 vendor selection 的四層問題：三型遷移路徑、dogfood signal 怎麼讀、multi-model 差異化、跨雲 hedging — 從 Microsoft 365 dogfood 案例切入">Cosmos DB MongoDB API vs SQL API</a>（multi-API adapter 取捨）</li>
</ul>
]]></content:encoded></item></channel></rss>