<?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>Sqlc on Tarragon</title><link>https://tarrragon.github.io/blog/tags/sqlc/</link><description>Recent content in Sqlc on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 06 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/sqlc/index.xml" rel="self" type="application/rss+xml"/><item><title>10.4 sqlc：從 SQL 產生型別安全的 Go 程式碼</title><link>https://tarrragon.github.io/blog/programming/go/basic/10-database-access/sqlc/</link><pubDate>Tue, 06 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/go/basic/10-database-access/sqlc/</guid><description>&lt;p>這篇整理 &lt;a href="https://sqlc.dev/">sqlc&lt;/a> v1.31.1 的用法。範例沿用 &lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/database-sql/#%e7%af%84%e4%be%8b%e8%b3%87%e6%96%99%e8%a1%a8" data-link-title="10.1 database/sql：連線池、查詢結果的讀取與交易" data-link-desc="Go 標準庫存取資料庫的模型：*sql.DB 是連線池、驅動怎麼註冊、Scan 與 NULL、查詢結果的讀取物件（*sql.Rows）沒關閉時的連線洩漏、交易的寫法，以及本模組其餘工具各自接手這一層的哪些工作">10.1 database/sql：連線池、查詢結果的讀取與交易&lt;/a> 定義的 &lt;code>customers&lt;/code> 與 &lt;code>orders&lt;/code> 兩張表，驅動用 &lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/pgx/" data-link-title="10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入" data-link-desc="pgx 當 database/sql 驅動與用原生介面的差別、pgxpool 連線池、依欄名把查詢結果對映進 struct、從 *pgconn.PgError 讀出約束名稱、交易輔助函式、CopyFrom 大量寫入，以及語句快取與連線池代理的關係">10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入&lt;/a> 的 pgx v5。在 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&amp;#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層&lt;/a> 的分類裡，sqlc 屬於從 SQL 產生程式碼：SQL 由人手寫在 &lt;code>.sql&lt;/code> 檔裡，sqlc 讀 schema 與這些 SQL，產生呼叫它們的 Go 函式，以及參數與結果的 struct。&lt;/p>
&lt;p>sqlc 是開發時執行的命令列工具，不是程式在執行時匯入的函式庫；執行時的程式碼只依賴它產生出來的檔案與 pgx。產生出來的 &lt;code>store&lt;/code> 套件放在專案的 Go 模組裡，呼叫端以 &lt;code>import &amp;quot;&amp;lt;go.mod 的 module 名稱&amp;gt;/sqlcdemo/store&amp;quot;&lt;/code> 引用。&lt;/p>
&lt;h2 id="設定檔與目錄">設定檔與目錄&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">migrations/ # 專案的 migration 目錄，與 sqlcdemo/ 同一層
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">├── 001_init.up.sql
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">└── 001_init.down.sql
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">sqlcdemo/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">├── sqlc.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">├── query/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">│ └── orders.sql # 手寫的查詢
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">└── store/ # sqlc 產生，不手動修改
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> ├── db.go
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> ├── models.go
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> └── orders.sql.go&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="nt">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;2&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">sql&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">engine&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;postgresql&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">schema&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;../migrations&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 建表的 SQL；指向 migration 目錄&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">queries&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;query&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 手寫查詢所在的目錄&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">gen&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">go&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">package&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;store&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">out&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;store&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 產生的程式碼放哪裡&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">sql_package&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;pgx/v5&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 產生的程式碼呼叫 pgx 的原生介面&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>不寫 &lt;code>sql_package&lt;/code> 時，產生的是呼叫 &lt;code>database/sql&lt;/code> 的程式碼。要和 GORM 在同一個交易裡執行就選它，見 &lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/choosing-data-access/" data-link-title="10.7 Go 專案的資料庫存取工具選型：GORM、sqlc 與 pgx 的組合" data-link-desc="把語言無關的資料存取選型軸套到 Go 的工具上：驅動入口選 pgx 原生介面還是 database/sql、主力選 GORM 還是 sqlc、為什麼用 GORM 與用 sqlc 的專案都把 schema 交給 migration 工具、常見的混用組合、混用時交易能不能共用，以及讀寫分離時 dbresolver 把查詢送到主庫或副本的規則">10.7 Go 專案的資料庫存取工具選型：GORM、sqlc 與 pgx 的組合&lt;/a>〈混用時共用同一個交易〉。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理 <a href="https://sqlc.dev/">sqlc</a> v1.31.1 的用法。範例沿用 <a href="/blog/programming/go/basic/10-database-access/database-sql/#%e7%af%84%e4%be%8b%e8%b3%87%e6%96%99%e8%a1%a8" data-link-title="10.1 database/sql：連線池、查詢結果的讀取與交易" data-link-desc="Go 標準庫存取資料庫的模型：*sql.DB 是連線池、驅動怎麼註冊、Scan 與 NULL、查詢結果的讀取物件（*sql.Rows）沒關閉時的連線洩漏、交易的寫法，以及本模組其餘工具各自接手這一層的哪些工作">10.1 database/sql：連線池、查詢結果的讀取與交易</a> 定義的 <code>customers</code> 與 <code>orders</code> 兩張表，驅動用 <a href="/blog/programming/go/basic/10-database-access/pgx/" data-link-title="10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入" data-link-desc="pgx 當 database/sql 驅動與用原生介面的差別、pgxpool 連線池、依欄名把查詢結果對映進 struct、從 *pgconn.PgError 讀出約束名稱、交易輔助函式、CopyFrom 大量寫入，以及語句快取與連線池代理的關係">10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入</a> 的 pgx v5。在 <a href="/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層</a> 的分類裡，sqlc 屬於從 SQL 產生程式碼：SQL 由人手寫在 <code>.sql</code> 檔裡，sqlc 讀 schema 與這些 SQL，產生呼叫它們的 Go 函式，以及參數與結果的 struct。</p>
<p>sqlc 是開發時執行的命令列工具，不是程式在執行時匯入的函式庫；執行時的程式碼只依賴它產生出來的檔案與 pgx。產生出來的 <code>store</code> 套件放在專案的 Go 模組裡，呼叫端以 <code>import &quot;&lt;go.mod 的 module 名稱&gt;/sqlcdemo/store&quot;</code> 引用。</p>
<h2 id="設定檔與目錄">設定檔與目錄</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">migrations/                 # 專案的 migration 目錄，與 sqlcdemo/ 同一層
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">├── 001_init.up.sql
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">└── 001_init.down.sql
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">sqlcdemo/
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">├── sqlc.yaml
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">├── query/
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">│   └── orders.sql      # 手寫的查詢
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">└── store/              # sqlc 產生，不手動修改
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    ├── db.go
</span></span><span class="line"><span class="ln">10</span><span class="cl">    ├── models.go
</span></span><span class="line"><span class="ln">11</span><span class="cl">    └── orders.sql.go</span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;2&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">sql</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span>- <span class="nt">engine</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;postgresql&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="nt">schema</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;../migrations&#34;</span><span class="w">   </span><span class="c"># 建表的 SQL；指向 migration 目錄</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="nt">queries</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;query&#34;</span><span class="w">          </span><span class="c"># 手寫查詢所在的目錄</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="nt">gen</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">      </span><span class="nt">go</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">        </span><span class="nt">package</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;store&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="nt">out</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;store&#34;</span><span class="w">            </span><span class="c"># 產生的程式碼放哪裡</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="nt">sql_package</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;pgx/v5&#34;</span><span class="w">   </span><span class="c"># 產生的程式碼呼叫 pgx 的原生介面</span></span></span></code></pre></div><p>不寫 <code>sql_package</code> 時，產生的是呼叫 <code>database/sql</code> 的程式碼。要和 GORM 在同一個交易裡執行就選它，見 <a href="/blog/programming/go/basic/10-database-access/choosing-data-access/" data-link-title="10.7 Go 專案的資料庫存取工具選型：GORM、sqlc 與 pgx 的組合" data-link-desc="把語言無關的資料存取選型軸套到 Go 的工具上：驅動入口選 pgx 原生介面還是 database/sql、主力選 GORM 還是 sqlc、為什麼用 GORM 與用 sqlc 的專案都把 schema 交給 migration 工具、常見的混用組合、混用時交易能不能共用，以及讀寫分離時 dbresolver 把查詢送到主庫或副本的規則">10.7 Go 專案的資料庫存取工具選型：GORM、sqlc 與 pgx 的組合</a>〈混用時共用同一個交易〉。</p>
<p><code>schema</code> 指向專案的 migration 目錄（資料表結構的版本化變更檔，見 <a href="/blog/backend/knowledge-cards/schema-migration/" data-link-title="Schema Migration" data-link-desc="說明資料庫結構如何隨應用程式版本安全演進">Schema Migration</a>），sqlc 依序讀裡面的檔案、算出最終的資料表結構。它認得幾種 migration 工具的檔名慣例，例如 golang-migrate 的 <code>001_init.up.sql</code> 與 <code>001_init.down.sql</code> 放在同一個目錄時，只讀 <code>up</code> 那一份；把 <code>down</code> 也讀進去的話，<code>DROP TABLE</code> 會讓後面的查詢全部找不到表。schema 的權威因此留在 migration，sqlc 只是讀它。</p>
<h2 id="查詢檔與註記">查詢檔與註記</h2>
<p>每一段查詢上方的註記（query annotation）決定產生的函式名稱與回傳形狀：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- name: ListOrdersByCustomer :many
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">customer_id</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="p">,</span><span class="w"> </span><span class="n">note</span><span class="p">,</span><span class="w"> </span><span class="n">ordered_at</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$</span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">id</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">-- name: GetCustomer :one
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">email</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$</span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">-- name: CreateCustomer :one
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="n">email</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="err">$</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="err">$</span><span class="mi">2</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="n">RETURNING</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">created_at</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w"></span><span class="c1">-- name: TotalByCustomer :many
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="p">,</span><span class="w"> </span><span class="k">sum</span><span class="p">(</span><span class="n">o</span><span class="p">.</span><span class="n">amount</span><span class="p">)</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="n">total</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="k">FROM</span><span class="w"> </span><span class="n">customers</span><span class="w"> </span><span class="k">c</span><span class="w"> </span><span class="k">JOIN</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="n">o</span><span class="w"> </span><span class="k">ON</span><span class="w"> </span><span class="n">o</span><span class="p">.</span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">id</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w"></span><span class="k">GROUP</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">c</span><span class="p">.</span><span class="n">name</span><span class="p">;</span></span></span></code></pre></div><table>
  <thead>
      <tr>
          <th>註記</th>
          <th>產生的函式回傳</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>:one</code></td>
          <td>一個 struct；查不到時回傳 <code>pgx.ErrNoRows</code></td>
      </tr>
      <tr>
          <td><code>:many</code></td>
          <td>struct 的 slice</td>
      </tr>
      <tr>
          <td><code>:exec</code></td>
          <td>只回傳錯誤</td>
      </tr>
      <tr>
          <td><code>:execrows</code></td>
          <td>影響的列數與錯誤</td>
      </tr>
      <tr>
          <td><code>:copyfrom</code></td>
          <td>用 <a href="/blog/programming/go/basic/10-database-access/pgx/" data-link-title="10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入" data-link-desc="pgx 當 database/sql 驅動與用原生介面的差別、pgxpool 連線池、依欄名把查詢結果對映進 struct、從 *pgconn.PgError 讀出約束名稱、交易輔助函式、CopyFrom 大量寫入，以及語句快取與連線池代理的關係">10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入</a> 介紹的 <code>COPY</code> 協定批次寫入</td>
      </tr>
  </tbody>
</table>
<p>產生指令在設定檔所在的目錄執行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 不安裝，直接執行指定版本；第一次要下載並編譯，需要一兩分鐘</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">go run github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 generate
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 安裝成執行檔之後，每次產生只要不到一秒；執行檔放在 $(go env GOPATH)/bin，那個目錄要在 PATH 裡</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1
</span></span><span class="line"><span class="ln">6</span><span class="cl">sqlc generate</span></span></code></pre></div><p>sqlc v1.31.1 要求 Go 1.26 以上來編譯它自己；手上的 Go 版本較舊時，Go 會依 <code>GOTOOLCHAIN</code> 的預設設定自動下載符合的工具鏈，這不影響專案本身用的 Go 版本。版本號寫在指令裡，團隊每個人產生的程式碼才會一致；產生出來的檔案開頭也會記下產生它的 sqlc 版本。</p>
<h2 id="產生出來的程式碼">產生出來的程式碼</h2>
<p><code>ListOrdersByCustomer</code> 產生的函式就是 <a href="/blog/programming/go/basic/10-database-access/database-sql/" data-link-title="10.1 database/sql：連線池、查詢結果的讀取與交易" data-link-desc="Go 標準庫存取資料庫的模型：*sql.DB 是連線池、驅動怎麼註冊、Scan 與 NULL、查詢結果的讀取物件（*sql.Rows）沒關閉時的連線洩漏、交易的寫法，以及本模組其餘工具各自接手這一層的哪些工作">10.1 database/sql：連線池、查詢結果的讀取與交易</a> 讀多列的標準寫法，只是由工具寫出來：</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">q</span> <span class="o">*</span><span class="nx">Queries</span><span class="p">)</span> <span class="nf">ListOrdersByCustomer</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">customerID</span> <span class="kt">int64</span><span class="p">)</span> <span class="p">([]</span><span class="nx">Order</span><span class="p">,</span> <span class="kt">error</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">	<span class="nx">rows</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">q</span><span class="p">.</span><span class="nx">db</span><span class="p">.</span><span class="nf">Query</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">listOrdersByCustomer</span><span class="p">,</span> <span class="nx">customerID</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">		<span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">	<span class="k">defer</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Close</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">	<span class="kd">var</span> <span class="nx">items</span> <span class="p">[]</span><span class="nx">Order</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">	<span class="k">for</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Next</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">		<span class="kd">var</span> <span class="nx">i</span> <span class="nx">Order</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">		<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">i</span><span class="p">.</span><span class="nx">ID</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">i</span><span class="p">.</span><span class="nx">CustomerID</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">i</span><span class="p">.</span><span class="nx">Amount</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">i</span><span class="p">.</span><span class="nx">Note</span><span class="p">,</span> <span class="o">&amp;</span><span class="nx">i</span><span class="p">.</span><span class="nx">OrderedAt</span><span class="p">);</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">			<span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">		<span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">		<span class="nx">items</span> <span class="p">=</span> <span class="nb">append</span><span class="p">(</span><span class="nx">items</span><span class="p">,</span> <span class="nx">i</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">rows</span><span class="p">.</span><span class="nf">Err</span><span class="p">();</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">		<span class="k">return</span> <span class="kc">nil</span><span class="p">,</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">	<span class="k">return</span> <span class="nx">items</span><span class="p">,</span> <span class="kc">nil</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>參數與回傳型別依查詢決定：查詢選的欄位剛好是一整張表時，回傳那張表對應的 struct（<code>Order</code>，定義在 <code>models.go</code>）；選了部分欄位或含運算結果時，另外產生一個 <code>XxxRow</code> struct（<code>TotalByCustomer</code> 回傳 <code>[]TotalByCustomerRow</code>，<code>sum(o.amount)</code> 推成 <code>int64</code>）；參數超過一個時產生一個 <code>XxxParams</code> struct（<code>CreateCustomerParams{Name, Email}</code>）。呼叫端的寫法如下：</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="nx">pool</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">pgxpool</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">dsn</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">q</span> <span class="o">:=</span> <span class="nx">store</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">pool</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">orders</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">q</span><span class="p">.</span><span class="nf">ListOrdersByCustomer</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">totals</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">q</span><span class="p">.</span><span class="nf">TotalByCustomer</span><span class="p">(</span><span class="nx">ctx</span><span class="p">)</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[{Name:佳穎 Total:800} {Name:宗翰 Total:250}] &lt;nil&gt;</span></span></code></pre></div><p><code>store.New</code> 接受的是 <code>DBTX</code> 這個介面（<code>Exec</code>、<code>Query</code>、<code>QueryRow</code> 三個方法），<code>*pgxpool.Pool</code> 與 <code>pgx.Tx</code> 都符合。交易裡執行時用 <code>q.WithTx(tx)</code> 取得一個綁在那個交易上的 <code>*Queries</code>，交易的開始與收尾仍然照 pgx 那一篇的寫法由程式負責。</p>
<h2 id="產生時擋下的錯誤">產生時擋下的錯誤</h2>
<p>sqlc 在產生時解析每一段查詢、對照 schema，欄位或表名寫錯就不產生程式碼。把 <code>GetCustomer</code> 那一行的 <code>name</code> 打成 <code>nmae</code> 再產生一次：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># package store
</span></span><span class="line"><span class="ln">2</span><span class="cl">query/orders.sql:8:12: column &#34;nmae&#34; does not exist</span></span></code></pre></div><p>同樣的錯字在 sqlx 或 pgx 裡是一個要等到執行那一行才會出現的錯誤。換成 sqlc 之後，schema 改了欄位名稱，重新產生時所有用到舊名稱的查詢一起報錯，連帶呼叫端用到的 struct 欄位也跟著改名，Go 編譯器接著指出每一個要改的呼叫位置。</p>
<h2 id="null-與型別對應">NULL 與型別對應</h2>
<p>pgx v5 的預設對應裡，可以是 NULL 的欄位與 <code>timestamptz</code> 都產生成 pgx 的 <code>pgtype</code> 型別：</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">type</span> <span class="nx">Order</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">	<span class="nx">ID</span>         <span class="kt">int64</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">	<span class="nx">CustomerID</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">	<span class="nx">Amount</span>     <span class="kt">int32</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">	<span class="nx">Note</span>       <span class="nx">pgtype</span><span class="p">.</span><span class="nx">Text</span>        <span class="c1">// 可以是 NULL 的 text</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">	<span class="nx">OrderedAt</span>  <span class="nx">pgtype</span><span class="p">.</span><span class="nx">Timestamptz</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>pgtype</code> 型別讓產生的 struct 依賴 pgx 的套件，放進 domain 型別或 JSON 回應前都要轉換一次。兩個設定改掉這個預設，加在前面 <code>sqlc.yaml</code> 的 <code>gen.go</code> 底下、和 <code>sql_package</code> 同一層：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">gen</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span><span class="nt">go</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">emit_pointers_for_null_types</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">   </span><span class="c"># 可以是 NULL 的欄位改成指標，例如 *string</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">    </span><span class="nt">overrides</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span>- <span class="nt">db_type</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;timestamptz&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">        </span><span class="nt">go_type</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;time.Time&#34;</span><span class="w">             </span><span class="c"># NOT NULL 的 timestamptz 對應成 time.Time</span></span></span></code></pre></div>




<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">type</span> <span class="nx">Order</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">	<span class="nx">ID</span>         <span class="kt">int64</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">	<span class="nx">CustomerID</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">	<span class="nx">Amount</span>     <span class="kt">int32</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">	<span class="nx">Note</span>       <span class="o">*</span><span class="kt">string</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">	<span class="nx">OrderedAt</span>  <span class="nx">time</span><span class="p">.</span><span class="nx">Time</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>依 <code>db_type</code> 寫的 override 預設只套用在 NOT NULL 的欄位；可以是 NULL 的 <code>timestamptz</code> 欄位要另寫一條加上 <code>nullable: true</code> 的 override（例如 <code>go_type: {type: &quot;time.Time&quot;, pointer: true}</code>），否則仍然是 <code>pgtype.Timestamptz</code>。</p>
<p><code>integer</code> 欄位產生的是 <code>int32</code>，因為 PostgreSQL 的 <code>integer</code> 就是 32 位元；要 <code>int64</code> 的話建表時用 <code>bigint</code>，或同樣用 <code>overrides</code> 指定。</p>
<h2 id="選擇性的篩選條件">選擇性的篩選條件</h2>
<p>sqlc 產生的函式裡，SQL 文字在產生時就固定了，「有給最低金額才加這個條件」這類依輸入增減的條件，寫法是把條件寫成「參數是 NULL 時永遠成立」。把下面這段加進 <code>query/orders.sql</code> 再重新產生：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- name: SearchOrders :many
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">amount</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">customer_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$</span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="p">(</span><span class="n">sqlc</span><span class="p">.</span><span class="n">narg</span><span class="p">(</span><span class="n">min_amount</span><span class="p">)::</span><span class="nb">int</span><span class="w"> </span><span class="k">IS</span><span class="w"> </span><span class="k">NULL</span><span class="w"> </span><span class="k">OR</span><span class="w"> </span><span class="n">amount</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">sqlc</span><span class="p">.</span><span class="n">narg</span><span class="p">(</span><span class="n">min_amount</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">id</span><span class="p">;</span></span></span></code></pre></div><p><code>sqlc.narg</code> 宣告一個可以是 NULL 的具名參數。在開了 <code>emit_pointers_for_null_types</code> 的設定下，產生的參數 struct 裡它是指標（預設是 <code>pgtype.Int4</code>）：</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">type</span> <span class="nx">SearchOrdersParams</span> <span class="kd">struct</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">	<span class="nx">CustomerID</span> <span class="kt">int64</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">	<span class="nx">MinAmount</span>  <span class="o">*</span><span class="kt">int32</span> <span class="c1">// nil 代表不篩選</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">MinAmount = nil  → [{101 300} {102 500}]
</span></span><span class="line"><span class="ln">2</span><span class="cl">MinAmount = 400  → [{102 500}]</span></span></code></pre></div><p>這個寫法在條件只有兩三個時清楚好讀。條件多到十幾個、排序欄位也由使用者決定時，<code>OR</code> 堆疊的查詢讀起來吃力，資料庫為它選的執行計畫也可能不是最好的；那類查詢交給查詢建構器（Query Builder）或 ORM 的條件串接，和 sqlc 並存（混用的方式見 <a href="/blog/programming/go/basic/10-database-access/choosing-data-access/" data-link-title="10.7 Go 專案的資料庫存取工具選型：GORM、sqlc 與 pgx 的組合" data-link-desc="把語言無關的資料存取選型軸套到 Go 的工具上：驅動入口選 pgx 原生介面還是 database/sql、主力選 GORM 還是 sqlc、為什麼用 GORM 與用 sqlc 的專案都把 schema 交給 migration 工具、常見的混用組合、混用時交易能不能共用，以及讀寫分離時 dbresolver 把查詢送到主庫或副本的規則">10.7 Go 專案的資料庫存取工具選型：GORM、sqlc 與 pgx 的組合</a>）。</p>
<h2 id="sqlc-帶進來的工作">sqlc 帶進來的工作</h2>
<p>sqlc 把 SQL 的錯誤提前到產生時，代價是多了一個產生步驟：</p>
<ul>
<li>schema 或查詢改了要重新產生，產生的檔案通常提交進版本控制，審查時看得到產生結果的差異。</li>
<li>CI 裡跑一次產生，再確認工作目錄沒有差異，可以擋下「改了 SQL 卻忘了重新產生」的提交。</li>
<li>產生的程式碼不手動修改；要改行為就改 SQL、設定檔，或在產生的函式外面再包一層。</li>
</ul>
<p>不想要這個步驟、而查詢以單表增刪查改為主的專案，看的是 <a href="/blog/programming/go/basic/10-database-access/gorm/" data-link-title="10.5 GORM：model、關聯載入與 ORM 送出的 SQL" data-link-desc="GORM 的 model 與命名慣例、開著 SQL 記錄寫程式的方式、First 與 Take 的差別與重用變數時多出的條件、找不到資料的判斷、N&#43;1 與 Preload、零值更新被略過、寫入時的預設交易、唯一約束錯誤的判讀，以及 AutoMigrate 在正式環境的限制">10.5 GORM：model、關聯載入與 ORM 送出的 SQL</a>。</p>
]]></content:encoded></item><item><title>10.7 Go 專案的資料庫存取工具選型：GORM、sqlc 與 pgx 的組合</title><link>https://tarrragon.github.io/blog/programming/go/basic/10-database-access/choosing-data-access/</link><pubDate>Tue, 06 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/go/basic/10-database-access/choosing-data-access/</guid><description>&lt;p>這篇把 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&amp;#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層&lt;/a> 的選型軸套到 Go 的工具上。那一篇給的是跨語言的軸（schema 的權威、查詢的形狀、框架的整合程度、審查要看到什麼）；Go 的選型多出兩件事要決定：驅動入口用哪一種，以及沒有主導框架時主力工具由誰來定；主力工具的選擇另外加上團隊背景這一軸。本模組前面各篇示範的行為是這篇的材料：&lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/database-sql/" data-link-title="10.1 database/sql：連線池、查詢結果的讀取與交易" data-link-desc="Go 標準庫存取資料庫的模型：*sql.DB 是連線池、驅動怎麼註冊、Scan 與 NULL、查詢結果的讀取物件（*sql.Rows）沒關閉時的連線洩漏、交易的寫法，以及本模組其餘工具各自接手這一層的哪些工作">10.1 database/sql：連線池、查詢結果的讀取與交易&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/pgx/" data-link-title="10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入" data-link-desc="pgx 當 database/sql 驅動與用原生介面的差別、pgxpool 連線池、依欄名把查詢結果對映進 struct、從 *pgconn.PgError 讀出約束名稱、交易輔助函式、CopyFrom 大量寫入，以及語句快取與連線池代理的關係">10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/sqlx/" data-link-title="10.3 sqlx：手寫 SQL 加上 struct 對映" data-link-desc="sqlx 在 database/sql 之上補的 struct 對映：Get 與 Select 依 db tag 把查詢結果放進 struct、查詢結果的欄位多於 struct 時的錯誤（missing destination name）與 SELECT * 的風險、具名參數與 IN 的展開，以及 SQL 仍是字串所留下的檢查缺口">10.3 sqlx：手寫 SQL 加上 struct 對映&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/sqlc/" data-link-title="10.4 sqlc：從 SQL 產生型別安全的 Go 程式碼" data-link-desc="sqlc 的工作流程：設定檔指向 schema 與查詢檔、查詢上方的註記決定產生的函式、產生出來的參數與結果型別、欄位寫錯在產生時就被擋下、NULL 與時間欄位的型別對應（預設的 pgtype 型別與改成指標的設定）、選擇性篩選條件的寫法，以及產生步驟帶進建置流程的代價">10.4 sqlc：從 SQL 產生型別安全的 Go 程式碼&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/gorm/" data-link-title="10.5 GORM：model、關聯載入與 ORM 送出的 SQL" data-link-desc="GORM 的 model 與命名慣例、開著 SQL 記錄寫程式的方式、First 與 Take 的差別與重用變數時多出的條件、找不到資料的判斷、N&amp;#43;1 與 Preload、零值更新被略過、寫入時的預設交易、唯一約束錯誤的判讀，以及 AutoMigrate 在正式環境的限制">10.5 GORM：model、關聯載入與 ORM 送出的 SQL&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/programming/go/basic/10-database-access/gorm-model-and-soft-delete/" data-link-title="10.6 GORM 的 gorm.Model、軟刪除與 Save：慣例欄位加上的查詢條件與整列寫回" data-link-desc="gorm.Model 帶進來的 ID、CreatedAt、UpdatedAt、DeletedAt 與 UpdatedAt 的更新時機、DeletedAt 讓刪除變成 UPDATE 並替每個查詢加上條件、不經過那個條件的路徑（Raw、其他服務）、軟刪除與關聯載入、外鍵、唯一約束的衝突與部分索引、無條件刪除的保護，以及 Save 寫回全部欄位、找不到主鍵時改成 upsert 而把已刪除的列寫回或恢復">10.6 GORM 的 gorm.Model、軟刪除與 Save：慣例欄位加上的查詢條件與整列寫回&lt;/a>。&lt;/p>
&lt;h2 id="go-資料庫存取工具的生態現況">Go 資料庫存取工具的生態現況&lt;/h2>
&lt;p>Go 沒有一個像 Laravel 或 Django 那樣把 ORM、驗證、後台整合在一起的主導框架，資料庫存取是各專案自己挑的。從 GitHub 的星數（2026 年 10 月查詢）看各工具受到的關注：GORM 約四萬，sqlc、sqlx、ent 各約一萬七千到一萬八千多，pgx 約一萬四千。星數量的是關注度，不是採用率；ent 是另一種 ORM，schema 寫成 Go 程式碼、再產生型別安全的查詢 API，同時帶有 ORM 與產生程式碼的性質，本模組不示範。pgx 也是 GORM 的 PostgreSQL 方言套件底下用的驅動，所以用 GORM 的專案同時也在用 pgx。&lt;/p>
&lt;p>本模組示範兩種常見的做法：用 GORM 這個關注度最高的 ORM，或用 sqlc 加 pgx 這組「SQL 手寫、工具產生程式碼」的組合。團隊選哪一種，比較的是驅動入口、查詢的形狀、審查要看到什麼、團隊背景與錯誤被發現的時間點。&lt;/p>
&lt;h2 id="驅動入口pgx-原生介面還是-databasesql">驅動入口：pgx 原生介面還是 database/sql&lt;/h2>
&lt;p>這個決定先做，因為它決定後面哪些函式庫用得上：&lt;/p></description><content:encoded><![CDATA[<p>這篇把 <a href="/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層</a> 的選型軸套到 Go 的工具上。那一篇給的是跨語言的軸（schema 的權威、查詢的形狀、框架的整合程度、審查要看到什麼）；Go 的選型多出兩件事要決定：驅動入口用哪一種，以及沒有主導框架時主力工具由誰來定；主力工具的選擇另外加上團隊背景這一軸。本模組前面各篇示範的行為是這篇的材料：<a href="/blog/programming/go/basic/10-database-access/database-sql/" data-link-title="10.1 database/sql：連線池、查詢結果的讀取與交易" data-link-desc="Go 標準庫存取資料庫的模型：*sql.DB 是連線池、驅動怎麼註冊、Scan 與 NULL、查詢結果的讀取物件（*sql.Rows）沒關閉時的連線洩漏、交易的寫法，以及本模組其餘工具各自接手這一層的哪些工作">10.1 database/sql：連線池、查詢結果的讀取與交易</a>、<a href="/blog/programming/go/basic/10-database-access/pgx/" data-link-title="10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入" data-link-desc="pgx 當 database/sql 驅動與用原生介面的差別、pgxpool 連線池、依欄名把查詢結果對映進 struct、從 *pgconn.PgError 讀出約束名稱、交易輔助函式、CopyFrom 大量寫入，以及語句快取與連線池代理的關係">10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入</a>、<a href="/blog/programming/go/basic/10-database-access/sqlx/" data-link-title="10.3 sqlx：手寫 SQL 加上 struct 對映" data-link-desc="sqlx 在 database/sql 之上補的 struct 對映：Get 與 Select 依 db tag 把查詢結果放進 struct、查詢結果的欄位多於 struct 時的錯誤（missing destination name）與 SELECT * 的風險、具名參數與 IN 的展開，以及 SQL 仍是字串所留下的檢查缺口">10.3 sqlx：手寫 SQL 加上 struct 對映</a>、<a href="/blog/programming/go/basic/10-database-access/sqlc/" data-link-title="10.4 sqlc：從 SQL 產生型別安全的 Go 程式碼" data-link-desc="sqlc 的工作流程：設定檔指向 schema 與查詢檔、查詢上方的註記決定產生的函式、產生出來的參數與結果型別、欄位寫錯在產生時就被擋下、NULL 與時間欄位的型別對應（預設的 pgtype 型別與改成指標的設定）、選擇性篩選條件的寫法，以及產生步驟帶進建置流程的代價">10.4 sqlc：從 SQL 產生型別安全的 Go 程式碼</a>、<a href="/blog/programming/go/basic/10-database-access/gorm/" data-link-title="10.5 GORM：model、關聯載入與 ORM 送出的 SQL" data-link-desc="GORM 的 model 與命名慣例、開著 SQL 記錄寫程式的方式、First 與 Take 的差別與重用變數時多出的條件、找不到資料的判斷、N&#43;1 與 Preload、零值更新被略過、寫入時的預設交易、唯一約束錯誤的判讀，以及 AutoMigrate 在正式環境的限制">10.5 GORM：model、關聯載入與 ORM 送出的 SQL</a>、<a href="/blog/programming/go/basic/10-database-access/gorm-model-and-soft-delete/" data-link-title="10.6 GORM 的 gorm.Model、軟刪除與 Save：慣例欄位加上的查詢條件與整列寫回" data-link-desc="gorm.Model 帶進來的 ID、CreatedAt、UpdatedAt、DeletedAt 與 UpdatedAt 的更新時機、DeletedAt 讓刪除變成 UPDATE 並替每個查詢加上條件、不經過那個條件的路徑（Raw、其他服務）、軟刪除與關聯載入、外鍵、唯一約束的衝突與部分索引、無條件刪除的保護，以及 Save 寫回全部欄位、找不到主鍵時改成 upsert 而把已刪除的列寫回或恢復">10.6 GORM 的 gorm.Model、軟刪除與 Save：慣例欄位加上的查詢條件與整列寫回</a>。</p>
<h2 id="go-資料庫存取工具的生態現況">Go 資料庫存取工具的生態現況</h2>
<p>Go 沒有一個像 Laravel 或 Django 那樣把 ORM、驗證、後台整合在一起的主導框架，資料庫存取是各專案自己挑的。從 GitHub 的星數（2026 年 10 月查詢）看各工具受到的關注：GORM 約四萬，sqlc、sqlx、ent 各約一萬七千到一萬八千多，pgx 約一萬四千。星數量的是關注度，不是採用率；ent 是另一種 ORM，schema 寫成 Go 程式碼、再產生型別安全的查詢 API，同時帶有 ORM 與產生程式碼的性質，本模組不示範。pgx 也是 GORM 的 PostgreSQL 方言套件底下用的驅動，所以用 GORM 的專案同時也在用 pgx。</p>
<p>本模組示範兩種常見的做法：用 GORM 這個關注度最高的 ORM，或用 sqlc 加 pgx 這組「SQL 手寫、工具產生程式碼」的組合。團隊選哪一種，比較的是驅動入口、查詢的形狀、審查要看到什麼、團隊背景與錯誤被發現的時間點。</p>
<h2 id="驅動入口pgx-原生介面還是-databasesql">驅動入口：pgx 原生介面還是 database/sql</h2>
<p>這個決定先做，因為它決定後面哪些函式庫用得上：</p>
<table>
  <thead>
      <tr>
          <th>條件</th>
          <th>選擇</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>會用到 GORM、sqlx，而且要和主程式在同一個交易裡寫入</td>
          <td><code>database/sql</code>（驅動用 pgx 的 <code>stdlib</code>）</td>
          <td>這些函式庫的交易是 <code>*sql.Tx</code>，和 <code>pgx.Tx</code> 不相容</td>
      </tr>
      <tr>
          <td>只連 PostgreSQL，主力是 sqlc 或直接寫 pgx</td>
          <td>pgx 原生介面（<code>pgxpool</code>）</td>
          <td>少一層轉換，<code>COPY</code>、陣列、<code>jsonb</code> 這些 PostgreSQL 功能直接可用（例如每晚用 <code>CopyFrom</code> 匯入數十萬筆對帳資料、把標籤存成 <code>text[]</code> 用 <code>= ANY($1)</code> 查詢）</td>
      </tr>
      <tr>
          <td>程式可能換資料庫，或要同時支援 MySQL 與 PostgreSQL</td>
          <td><code>database/sql</code></td>
          <td>換驅動時 Go 的呼叫方式（<code>QueryContext</code>、<code>Scan</code>、<code>*sql.Tx</code>）不變；SQL 字串裡的佔位符（<code>$1</code> 對 <code>?</code>）與方言（<code>RETURNING</code>、<code>ANY</code>）仍要逐段改。例如賣給客戶自行部署的軟體，要接客戶手上現有的資料庫</td>
      </tr>
  </tbody>
</table>
<p>只要共用連線池、不必共用交易時，主程式可以用 <code>pgxpool</code>，再用 <code>stdlib.OpenDBFromPool(pool)</code> 做出一個共用同一個池的 <code>*sql.DB</code>，交給 GORM（<code>postgres.New(postgres.Config{Conn: sqlDB})</code>）或 sqlx；實測兩者共用之後池裡只開一條連線。migration 工具多半在部署步驟或啟動時另開一個 <code>*sql.DB</code>、跑完就關，不影響主程式選哪個入口。應用程式和 PostgreSQL 之間有一層交易模式的 PgBouncer 時，兩個入口都要另外處理：pgx 不論走哪個入口，預設都會在連線上快取預先解析的查詢，實測會出現 <code>prepared statement &quot;stmtcache_…&quot; already exists (SQLSTATE 42P05)</code>，處理方式見 <a href="/blog/programming/go/basic/10-database-access/pgx/" data-link-title="10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入" data-link-desc="pgx 當 database/sql 驅動與用原生介面的差別、pgxpool 連線池、依欄名把查詢結果對映進 struct、從 *pgconn.PgError 讀出約束名稱、交易輔助函式、CopyFrom 大量寫入，以及語句快取與連線池代理的關係">10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入</a>〈pgx 的語句快取與 PgBouncer 交易模式〉。兩個入口可以在同一個程式裡並存；分別用 <code>sql.Open</code> 與 <code>pgxpool.New</code> 建立時各有一個連線池（連線數要合起來算），而不論池是否共用，交易都不能互通：<code>pgx.Tx</code> 與 <code>*sql.Tx</code> 是兩個不相容的型別，一段要在同一個交易裡完成的寫入，必須全部走同一個入口。</p>
<h2 id="主力工具gorm-還是-sqlc">主力工具：GORM 還是 sqlc</h2>
<p><a href="/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層</a> 的選型軸套到這兩個工具上：</p>
<table>
  <thead>
      <tr>
          <th>讀者要決定的事</th>
          <th>條件</th>
          <th>傾向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>查詢的形狀</td>
          <td>多數是單表或一層關聯的增刪查改，後台管理頁面多</td>
          <td>GORM</td>
      </tr>
      <tr>
          <td></td>
          <td>報表、多表聚合、視窗函數佔了查詢的一大部分</td>
          <td>sqlc</td>
      </tr>
      <tr>
          <td></td>
          <td>列表頁的篩選條件多、依使用者輸入動態組合</td>
          <td>GORM 的條件串接，或另加 Query Builder</td>
      </tr>
      <tr>
          <td>審查要看到什麼</td>
          <td>審查者要在 diff 裡直接看到每一段 SQL</td>
          <td>sqlc</td>
      </tr>
      <tr>
          <td></td>
          <td>審查看方法呼叫，SQL 靠開發時 GORM 的 SQL 記錄確認</td>
          <td>GORM</td>
      </tr>
      <tr>
          <td>團隊的背景</td>
          <td>成員從 Laravel、Rails、Django 轉過來，習慣 ORM 的寫法</td>
          <td>GORM</td>
      </tr>
      <tr>
          <td></td>
          <td>成員熟 SQL，查詢常要調執行計畫</td>
          <td>sqlc</td>
      </tr>
      <tr>
          <td>錯誤在什麼時候被發現</td>
          <td>希望欄位改名、查詢寫錯在產生程式碼時（<code>sqlc generate</code>）就失敗</td>
          <td>sqlc</td>
      </tr>
      <tr>
          <td></td>
          <td>接受由整合測試在執行時抓出來</td>
          <td>GORM</td>
      </tr>
  </tbody>
</table>
<p>團隊背景這一軸的理由在於改 SQL 的位置：用 sqlc 時，送出的 SQL 就是 <code>.sql</code> 檔裡那一段，調執行計畫就是改那一段；用 GORM 時要先把那個查詢改寫成 <code>Raw</code>。熟 SQL 的團隊在前一種位置上工作，習慣 ORM 的團隊在後一種。</p>
<p>表裡沒有 sqlx，因為它和 sqlc 處在同一側（SQL 手寫、審查看得到），差別在檢查的時間點與步驟：sqlx 不需要產生步驟，欄位錯誤到執行時才出現；只連 PostgreSQL 時，pgx 原生介面的 <code>RowToStructByName</code> 提供同樣的對映而不必多一個函式庫。所以 sqlx 的落點是：要走 <code>database/sql</code>（例如和 GORM 共用連線池，或要支援 PostgreSQL 以外的資料庫）、想手寫 SQL、又不想加產生步驟。</p>
<p>不同的軸給出相反的傾向時（例如團隊的背景傾向 GORM、查詢的形狀傾向 sqlc），不必二選一，看下面的混用組合。判錯的代價各軸不同：後台列表有十幾個篩選條件卻全交給 sqlc，每加一個條件就多一組 <code>sqlc.narg ... IS NULL OR</code>，查詢越來越長、執行計畫也可能變差；以報表為主的服務選了 GORM，多數查詢最後寫進 <code>Raw</code>，SQL 回到字串，產生時的檢查與 ORM 的對映兩邊都沒拿到；習慣 ORM 的團隊被要求全用 sqlc，初期每一段增刪查改都要寫 SQL 加註記，交付速度明顯變慢。</p>
<h2 id="go-專案裡-schema-的權威migration-檔gorm-model-與-atlas">Go 專案裡 schema 的權威：migration 檔、GORM model 與 Atlas</h2>
<p><a href="/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層</a> 把「schema 的權威放哪裡」列為選型軸：schema 由 model 定義時傾向 ORM，由 migration 的 SQL 定義時傾向從 SQL 產生程式碼。GORM 本身的 <code>AutoMigrate</code> 不適合正式環境（改名變成新增欄位、沒有變更記錄與退回方法，見 <a href="/blog/programming/go/basic/10-database-access/gorm/" data-link-title="10.5 GORM：model、關聯載入與 ORM 送出的 SQL" data-link-desc="GORM 的 model 與命名慣例、開著 SQL 記錄寫程式的方式、First 與 Take 的差別與重用變數時多出的條件、找不到資料的判斷、N&#43;1 與 Preload、零值更新被略過、寫入時的預設交易、唯一約束錯誤的判讀，以及 AutoMigrate 在正式環境的限制">10.5 GORM：model、關聯載入與 ORM 送出的 SQL</a>），所以用 GORM 的專案有兩種做法。一種是同樣用版本化 migration 工具、由手寫的 migration 檔當 schema 的權威，這時這個軸分不開 GORM 與 sqlc。另一種是用 Atlas 的 GORM provider（<code>ariga/atlas-provider-gorm</code>）從 model 算出版本化的 migration 檔，model 仍然是 schema 的權威，產生出來的每一份檔案照樣要審查；這時軸又分得開：model 是權威選 GORM 加 Atlas，migration 的 SQL 是權威選 sqlc。golang-migrate 與 goose 都是依序執行手寫的 SQL 檔；Atlas 另外能比對資料庫現況與期望的 schema、產生差異的 migration，三者在這個用途上擇一即可。</p>
<p>以 migration 檔當權威時，用 GORM 與用 sqlc 的差別落在「model 怎麼跟上 schema」：</p>
<ul>
<li><strong>sqlc</strong>：讀 migration 目錄產生 struct，schema 改了重新產生，對不上的地方在產生或編譯時報錯。</li>
<li><strong>GORM</strong>：model 是手寫的 struct，schema 改了要手動同步 model，對不上的地方在執行時才出現，而且不一定報錯：欄位從資料表移除之後，讀取送出的是 <code>SELECT *</code>，不會報錯，那個欄位保持零值，寫入時才報錯；資料表多出來的欄位則一律被忽略。</li>
</ul>
<p>用 GORM 的專案通常靠整合測試補上「model 與 schema 不一致要到執行時才發現」這個缺口：在測試環境依序套用全部 migration，再讓每個 model 實際讀寫一次。</p>
<p>以上都假設 schema 由這個 Go 服務擁有。資料庫被多個服務共用、schema 由另一個服務（例如 Laravel 的 migration）管理時，Go 這一側不跑 migration、也不用 <code>AutoMigrate</code>：sqlc 的 <code>schema</code> 改指向 <code>pg_dump --schema-only</code> 匯出的檔案，GORM 的 model 則靠整合測試對著擁有方建出的 schema 驗證；約束名稱也要照擁有方的命名判讀（Laravel 預設把唯一約束命名成 <code>customers_email_unique</code>）。</p>
<h2 id="常見的混用組合">常見的混用組合</h2>
<p>多數 Go 專案不只用一個工具。三種組合最常見：</p>
<table>
  <thead>
      <tr>
          <th>組合</th>
          <th>分工</th>
          <th>要留意的地方</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>GORM 加 <code>Raw</code></td>
          <td>增刪查改用 GORM，報表用 <code>db.Raw(sql).Scan(&amp;rows)</code> 直接寫 SQL</td>
          <td><code>Raw</code> 裡的 SQL 是字串，錯誤到執行時才出現，和 sqlx 一樣；model 用軟刪除時，<code>Raw</code> 不會自動加 <code>deleted_at IS NULL</code>，要自己寫（見 10.6〈不經過軟刪除條件的路徑〉）</td>
      </tr>
      <tr>
          <td>GORM 加 sqlc</td>
          <td>增刪查改用 GORM，固定而複雜的查詢用 sqlc</td>
          <td>sqlc 要產生 <code>database/sql</code> 版本的程式碼，才能和 GORM 共用交易（見〈混用時共用同一個交易〉）；GORM 的 model 用軟刪除時，sqlc 的查詢要自己寫 <code>deleted_at IS NULL</code></td>
      </tr>
      <tr>
          <td>sqlc 加 pgx 加 Query Builder</td>
          <td>多數查詢用 sqlc，條件多變的搜尋用 squirrel 這類 Query Builder 組出 SQL 再交給 pgx 執行</td>
          <td>Query Builder 組出的 SQL 沒有產生時的檢查，要有測試涵蓋；它在審查時看不到最終的 SQL，要求每段 SQL 在審查時可見的服務不適合；squirrel 預設的佔位符是 <code>?</code>，接 PostgreSQL 要設 <code>PlaceholderFormat(sq.Dollar)</code></td>
      </tr>
  </tbody>
</table>
<h2 id="混用時共用同一個交易">混用時共用同一個交易</h2>
<p>GORM 與 sqlc 寫在同一個交易裡的前提，是兩者都走 <code>database/sql</code>。sqlc 的設定檔不寫 <code>sql_package</code>（或寫 <code>database/sql</code>）時，產生的 <code>DBTX</code> 介面用的是 <code>ExecContext</code>、<code>QueryContext</code> 這組 <code>database/sql</code> 的方法，<code>*sql.Tx</code> 符合它。GORM 的交易函式裡，<code>tx.Statement.ConnPool</code> 就是那個 <code>*sql.Tx</code>（<code>gorm.Config</code> 開了 <code>PrepareStmt</code> 時不是，下面的型別斷言會 panic）。sqlc 這一側的查詢檔裡有這一段：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- name: AddNote :exec
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">note</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$</span><span class="mi">2</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="err">$</span><span class="mi">1</span><span class="p">;</span></span></span></code></pre></div><p>兩者寫在同一個交易裡：</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="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">WithContext</span><span class="p">(</span><span class="nx">ctx</span><span class="p">).</span><span class="nf">Transaction</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="o">*</span><span class="nx">gorm</span><span class="p">.</span><span class="nx">DB</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">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Model</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">Order</span><span class="p">{</span><span class="nx">ID</span><span class="p">:</span> <span class="mi">103</span><span class="p">}).</span><span class="nf">Update</span><span class="p">(</span><span class="s">&#34;amount&#34;</span><span class="p">,</span> <span class="mi">999</span><span class="p">).</span><span class="nx">Error</span><span class="p">;</span> <span class="nx">err</span> <span class="o">!=</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">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">	<span class="nx">sqlTx</span> <span class="o">:=</span> <span class="nx">tx</span><span class="p">.</span><span class="nx">Statement</span><span class="p">.</span><span class="nx">ConnPool</span><span class="p">.(</span><span class="o">*</span><span class="nx">sql</span><span class="p">.</span><span class="nx">Tx</span><span class="p">)</span> <span class="c1">// GORM 這個交易底下的 *sql.Tx</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">	<span class="nx">q</span> <span class="o">:=</span> <span class="nx">store</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">sqlTx</span><span class="p">)</span>                     <span class="c1">// sqlc 產生的程式碼跑在同一個交易裡</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">	<span class="k">if</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">q</span><span class="p">.</span><span class="nf">AddNote</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">store</span><span class="p">.</span><span class="nx">AddNoteParams</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">		<span class="nx">ID</span><span class="p">:</span>   <span class="mi">103</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">		<span class="nx">Note</span><span class="p">:</span> <span class="nx">sql</span><span class="p">.</span><span class="nx">NullString</span><span class="p">{</span><span class="nx">String</span><span class="p">:</span> <span class="s">&#34;同一個交易&#34;</span><span class="p">,</span> <span class="nx">Valid</span><span class="p">:</span> <span class="kc">true</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">	<span class="p">});</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">		<span class="k">return</span> <span class="nx">err</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">	<span class="p">}</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">	<span class="k">return</span> <span class="nx">errors</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="s">&#34;撤銷&#34;</span><span class="p">)</span> <span class="c1">// 回傳錯誤：GORM 與 sqlc 的兩筆修改一起撤銷</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">})</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">ConnPool is *sql.Tx: true
</span></span><span class="line"><span class="ln">2</span><span class="cl">tx err: 撤銷 amount: 250 note nil: true</span></span></code></pre></div><p>金額仍是原來的 250、備註仍是 NULL，兩邊的修改都被撤銷了。sqlc 改成產生 pgx 原生版本（<code>sql_package: &quot;pgx/v5&quot;</code>）的話，它要的是 <code>pgx.Tx</code>，和 GORM 的 <code>*sql.Tx</code> 接不起來；這時有兩個做法：GORM 與 sqlc 各自開交易，接受它們不在同一個交易裡；或把需要一起成功的寫入全部交給 GORM，或全部交給 sqlc。</p>
<h2 id="讀寫分離gorm-的-dbresolver-與程式自己持有兩個池">讀寫分離：GORM 的 dbresolver 與程式自己持有兩個池</h2>
<p>讀取量大到要把查詢分給唯讀副本（replica）時，用 GORM 的專案可以用 GORM 官方維護的 dbresolver 外掛；主力是 sqlc 或 pgx 時沒有這樣的外掛，由程式自己持有兩個池。dbresolver 的路由規則在外掛裡，自己持有兩個池時寫在程式碼上，語言無關的比較見 <a href="/blog/backend/01-database/data-access-layers/" data-link-title="1.17 應用程式存取資料庫的工具分層：驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM" data-link-desc="應用程式存取關聯式資料庫的工作（寫 SQL、取得連線與交易、把結果對映成程式的值），驅動、結果對映、從 SQL 產生程式碼、Query Builder 與 ORM 各接手哪幾件、接手之後哪些行為看不見，ORM 的 N&#43;1 與交易邊界、Go、gunicorn 與 PHP-FPM 各自對資料庫的連線總數與 max_connections、唯一約束違反時各語言讀取約束名稱的位置、讀寫分離時各框架把查詢送到主庫或副本的規則、資料拆到多個資料庫時跨不過的邊界，以及 Go、PHP（Laravel）與 Python 的對應工具與選型軸">1.17 應用程式存取資料庫的工具分層</a>〈讀寫分離與多個資料庫：連線的路由由哪一層決定〉。</p>
<p>dbresolver 註冊之後，程式照常呼叫 GORM，外掛依查詢的種類選連線。<code>db</code> 原本用主庫的連線字串開啟；dbresolver 的設定裡另有一個 <code>Sources</code> 欄位可以列主庫，沒有列時主庫就是 <code>db</code> 本身的連線；<code>replicaDSN</code> 換成副本的連線字串，有多個副本就列多個：</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="nx">db</span><span class="p">.</span><span class="nf">Use</span><span class="p">(</span><span class="nx">dbresolver</span><span class="p">.</span><span class="nf">Register</span><span class="p">(</span><span class="nx">dbresolver</span><span class="p">.</span><span class="nx">Config</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">	<span class="nx">Replicas</span><span class="p">:</span> <span class="p">[]</span><span class="nx">gorm</span><span class="p">.</span><span class="nx">Dialector</span><span class="p">{</span><span class="nx">postgres</span><span class="p">.</span><span class="nf">Open</span><span class="p">(</span><span class="nx">replicaDSN</span><span class="p">)},</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">}))</span></span></span></code></pre></div><p>下面的實測用兩個內容不同的資料庫當主庫與副本（取得套件：<code>go get gorm.io/plugin/dbresolver@v1.6.2</code>）：副本裡每位顧客的名字後面多了「（副本）」，查詢讀到哪一個名字，就知道它被送到哪一邊。</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">var</span> <span class="nx">c</span> <span class="nx">Customer</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">var</span> <span class="nx">name</span> <span class="kt">string</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nx">n</span> <span class="o">:=</span> <span class="nx">Customer</span><span class="p">{</span><span class="nx">Name</span><span class="p">:</span> <span class="s">&#34;新顧客&#34;</span><span class="p">,</span> <span class="nx">Email</span><span class="p">:</span> <span class="s">&#34;new@example.com&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">c</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>                                <span class="c1">// 一般查詢</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">Clauses</span><span class="p">(</span><span class="nx">dbresolver</span><span class="p">.</span><span class="nx">Write</span><span class="p">).</span><span class="nf">First</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">c</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>      <span class="c1">// 指定主庫</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">n</span><span class="p">);</span> <span class="nx">db</span><span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">c</span><span class="p">,</span> <span class="nx">n</span><span class="p">.</span><span class="nx">ID</span><span class="p">)</span>              <span class="c1">// 剛建立就讀</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">Transaction</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">tx</span> <span class="o">*</span><span class="nx">gorm</span><span class="p">.</span><span class="nx">DB</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">First</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">c</span><span class="p">,</span> <span class="mi">1</span><span class="p">);</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">})</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">Raw</span><span class="p">(</span><span class="s">&#34;SELECT name FROM customers WHERE id = 1&#34;</span><span class="p">).</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">name</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nx">db</span><span class="p">.</span><span class="nf">Raw</span><span class="p">(</span><span class="s">&#34;WITH x AS (SELECT 1) SELECT name FROM customers WHERE id = 1&#34;</span><span class="p">).</span><span class="nf">Scan</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">name</span><span class="p">)</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">First                                    &#34;佳穎（副本）&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">Clauses(dbresolver.Write).First          &#34;佳穎&#34;
</span></span><span class="line"><span class="ln">3</span><span class="cl">Create then First(new id)                id=4 err=record not found
</span></span><span class="line"><span class="ln">4</span><span class="cl">First inside Transaction                 &#34;佳穎&#34;
</span></span><span class="line"><span class="ln">5</span><span class="cl">Raw SELECT                               &#34;佳穎（副本）&#34;
</span></span><span class="line"><span class="ln">6</span><span class="cl">Raw WITH ... SELECT                      &#34;佳穎&#34;</span></span></code></pre></div><p>一般查詢送副本、寫入與交易裡的查詢送主庫。剛建立的顧客馬上用 <code>First</code> 讀，查詢送到副本而查不到；真實的副本在複製追上之後就讀得到，所以這種錯誤只在寫入後立刻讀、而且複製剛好落後的時候出現。同一個請求裡寫完就讀的查詢要寫 <code>Clauses(dbresolver.Write)</code>，或整段放進交易；寫完之後由下一個請求讀的路徑（建立後導向詳情頁、改完設定馬上重新載入），dbresolver 不知道上一個請求寫過，要程式自己記住這個使用者剛寫過（例如在 session 記下寫入時間，幾秒內的讀取都加 <code>Clauses(dbresolver.Write)</code>）。<code>Raw</code> 的路由看 SQL 文字：外掛的原始碼只在 SQL 以 <code>SELECT</code> 開頭、結尾不是 <code>FOR UPDATE</code> 時送副本，所以 <code>WITH</code> 開頭的查詢送主庫。</p>
<p>程式自己持有兩個池時，這些規則都寫在程式碼上：sqlc 產生的 <code>New</code> 收一個連線介面（<code>DBTX</code>），連線池與交易都符合它，呼叫端決定傳哪一個。</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="nx">primary</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">pgxpool</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">primaryDSN</span><span class="p">)</span> <span class="c1">// 主庫</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">replica</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">pgxpool</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="nx">replicaDSN</span><span class="p">)</span> <span class="c1">// 副本</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nx">orders</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">store</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">replica</span><span class="p">).</span><span class="nf">ListOrdersByCustomer</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="c1">// 可以接受落後的訂單列表</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nx">c</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">store</span><span class="p">.</span><span class="nf">New</span><span class="p">(</span><span class="nx">primary</span><span class="p">).</span><span class="nf">GetCustomer</span><span class="p">(</span><span class="nx">ctx</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>               <span class="c1">// 剛寫完就讀的那一筆</span></span></span></code></pre></div><p><code>store</code> 是 <a href="/blog/programming/go/basic/10-database-access/sqlc/" data-link-title="10.4 sqlc：從 SQL 產生型別安全的 Go 程式碼" data-link-desc="sqlc 的工作流程：設定檔指向 schema 與查詢檔、查詢上方的註記決定產生的函式、產生出來的參數與結果型別、欄位寫錯在產生時就被擋下、NULL 與時間欄位的型別對應（預設的 pgtype 型別與改成指標的設定）、選擇性篩選條件的寫法，以及產生步驟帶進建置流程的代價">10.4 sqlc：從 SQL 產生型別安全的 Go 程式碼</a> 以 <code>sql_package: &quot;pgx/v5&quot;</code> 產生的套件，<code>ListOrdersByCustomer</code> 與 <code>GetCustomer</code> 是那一篇定義的查詢。每一個查詢送到哪裡都在呼叫的那一行看得到，代價是每個呼叫端都要做這個選擇。主庫與副本各有一個池，連線數要分開算上限；dbresolver 的情形也一樣，<code>db.DB()</code> 只回傳主庫的 <code>*sql.DB</code>，副本的池要用 dbresolver 的 <code>SetMaxOpenConns</code> 這組方法設定。這組方法對主庫與每一個副本設同一個值（設定裡沒有列 <code>Sources</code> 時主庫也包含在內），主庫要另一個上限時，在呼叫 dbresolver 的 <code>SetMaxOpenConns</code> 之後，用 <code>db.DB()</code> 取得的 <code>*sql.DB</code> 重設。</p>
<h2 id="選型結果裡要統一的項目連線池例外路徑migration-工具錯誤值null交易傳遞唯讀副本與記錄">選型結果裡要統一的項目：連線池、例外路徑、migration 工具、錯誤值、NULL、交易傳遞、唯讀副本與記錄</h2>
<p>選型的結果不只是一個工具名稱，因為同一個工具兩個開發者各自決定細節時，會在下面這幾處分岔：</p>
<ul>
<li><strong>連線池由哪一段程式建立、上限多少</strong>：兩處各建一個池，連線數加倍、各自的上限也就失效（池的設定見 <a href="/blog/programming/go/basic/10-database-access/database-sql/" data-link-title="10.1 database/sql：連線池、查詢結果的讀取與交易" data-link-desc="Go 標準庫存取資料庫的模型：*sql.DB 是連線池、驅動怎麼註冊、Scan 與 NULL、查詢結果的讀取物件（*sql.Rows）沒關閉時的連線洩漏、交易的寫法，以及本模組其餘工具各自接手這一層的哪些工作">10.1 database/sql：連線池、查詢結果的讀取與交易</a>）。</li>
<li><strong>主力工具與例外</strong>：一人把報表寫成 GORM 的 <code>Raw</code>、另一人寫成 sqlc，同一類查詢有兩種檢查時間點，例外要寫成規則（例如「報表類查詢放 <code>query/</code> 用 sqlc」）。</li>
<li><strong>schema 由哪個 migration 工具管理</strong>：一人用 <code>AutoMigrate</code>、另一人寫 migration 檔，schema 會漂移，改名還會留下舊欄（見 <a href="/blog/programming/go/basic/10-database-access/gorm/" data-link-title="10.5 GORM：model、關聯載入與 ORM 送出的 SQL" data-link-desc="GORM 的 model 與命名慣例、開著 SQL 記錄寫程式的方式、First 與 Take 的差別與重用變數時多出的條件、找不到資料的判斷、N&#43;1 與 Preload、零值更新被略過、寫入時的預設交易、唯一約束錯誤的判讀，以及 AutoMigrate 在正式環境的限制">10.5 GORM：model、關聯載入與 ORM 送出的 SQL</a>〈AutoMigrate 在正式環境的限制〉）。</li>
<li><strong>唯一約束的錯誤怎麼判讀</strong>：一人讀錯誤代碼、另一人讀約束名稱，回報給使用者的訊息就不一致（見 <a href="/blog/programming/go/basic/10-database-access/pgx/" data-link-title="10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入" data-link-desc="pgx 當 database/sql 驅動與用原生介面的差別、pgxpool 連線池、依欄名把查詢結果對映進 struct、從 *pgconn.PgError 讀出約束名稱、交易輔助函式、CopyFrom 大量寫入，以及語句快取與連線池代理的關係">10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入</a>〈錯誤：*pgconn.PgError〉）。</li>
<li><strong>前面有沒有交易模式的連線池代理，以及驅動的查詢執行模式</strong>：一人在本機直連 PostgreSQL、另一人在正式環境經過 PgBouncer，預先解析查詢的錯誤只在經過 PgBouncer 的正式環境出現（見 <a href="/blog/programming/go/basic/10-database-access/pgx/" data-link-title="10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入" data-link-desc="pgx 當 database/sql 驅動與用原生介面的差別、pgxpool 連線池、依欄名把查詢結果對映進 struct、從 *pgconn.PgError 讀出約束名稱、交易輔助函式、CopyFrom 大量寫入，以及語句快取與連線池代理的關係">10.2 pgx：PostgreSQL 驅動的原生介面、連線池與 COPY 大量寫入</a>）。</li>
<li><strong>主力是 GORM 時，開發與測試環境的兩種記錄</strong>：GORM 的 SQL 記錄看得到送出的語句，零值更新被略過時那裡會少一行 <code>UPDATE</code>；預設交易的 <code>begin</code> 與 <code>commit</code> 只在資料庫的語句記錄（<code>log_statement</code>）裡看得到。</li>
<li><strong>NULL 的表示</strong>：<code>sql.NullString</code>、<code>*string</code> 與 sqlc 預設的 <code>pgtype.Text</code> 混在同一個專案裡，轉成 domain 型別的程式就要寫三套（見 <a href="/blog/programming/go/basic/10-database-access/database-sql/" data-link-title="10.1 database/sql：連線池、查詢結果的讀取與交易" data-link-desc="Go 標準庫存取資料庫的模型：*sql.DB 是連線池、驅動怎麼註冊、Scan 與 NULL、查詢結果的讀取物件（*sql.Rows）沒關閉時的連線洩漏、交易的寫法，以及本模組其餘工具各自接手這一層的哪些工作">10.1 database/sql：連線池、查詢結果的讀取與交易</a>〈Scan 與 NULL〉）。</li>
<li><strong>「找不到」的錯誤值</strong>：<code>sql.ErrNoRows</code>、<code>pgx.ErrNoRows</code>、<code>gorm.ErrRecordNotFound</code>，以及 GORM 的 <code>Find</code> 不回錯誤；混用的專案三種都會出現，repository 要在邊界把它們轉成同一個錯誤。</li>
<li><strong>交易物件怎麼傳進各個 repository</strong>：一人用參數傳 <code>*sql.Tx</code>、另一人放進 <code>context</code>，混用時就接不起來（傳遞方式見 <a href="/blog/backend/01-database/repository-adapter/" data-link-title="1.4 Repository Adapter 實作" data-link-desc="Port / Adapter 邊界、row mapping、error translation、ORM vs query builder 選型、contract test 設計">1.4 Repository Adapter 實作</a>〈Transaction 傳遞〉，誤用時的徵兆見 <a href="/blog/backend/knowledge-cards/transaction-propagation/" data-link-title="Transaction Propagation（交易傳遞）" data-link-desc="多個 repository 的寫入要在同一個交易裡完成，或一段程式在交易失敗後仍留下部分寫入、交易裡的某個查詢卡住直到逾時時，查交易物件怎麼傳到每一個資料存取呼叫">Transaction Propagation</a>）。</li>
<li><strong>要不要用 <code>gorm.Model</code> 的軟刪除、<code>Save</code> 用在哪裡</strong>：一人用軟刪除、另一人寫的報表 SQL 不加 <code>deleted_at IS NULL</code>，或一人用 <code>Save</code> 寫回整列、另一人只改欄位，同一張表上就會出現已刪除的資料被算進報表、別人的修改被蓋掉（見 <a href="/blog/programming/go/basic/10-database-access/gorm-model-and-soft-delete/" data-link-title="10.6 GORM 的 gorm.Model、軟刪除與 Save：慣例欄位加上的查詢條件與整列寫回" data-link-desc="gorm.Model 帶進來的 ID、CreatedAt、UpdatedAt、DeletedAt 與 UpdatedAt 的更新時機、DeletedAt 讓刪除變成 UPDATE 並替每個查詢加上條件、不經過那個條件的路徑（Raw、其他服務）、軟刪除與關聯載入、外鍵、唯一約束的衝突與部分索引、無條件刪除的保護，以及 Save 寫回全部欄位、找不到主鍵時改成 upsert 而把已刪除的列寫回或恢復">10.6 GORM 的 gorm.Model、軟刪除與 Save：慣例欄位加上的查詢條件與整列寫回</a>）。</li>
<li><strong>有唯讀副本時，哪些路徑強制讀主庫</strong>：寫入後立刻讀的路徑一人加了 <code>Clauses(dbresolver.Write)</code>、另一人沒加，同一類畫面時好時壞，只在複製落後時出錯（見〈讀寫分離：GORM 的 dbresolver 與程式自己持有兩個池〉）。</li>
<li><strong><code>gorm.Config</code> 的旗標由同一處設定</strong>：<code>PrepareStmt</code>、<code>SkipDefaultTransaction</code>、<code>TranslateError</code> 都會改變行為；開了 <code>PrepareStmt</code>，〈混用時共用同一個交易〉的 <code>ConnPool.(*sql.Tx)</code> 型別斷言會 panic。</li>
</ul>
<p>這幾條決定了 repository 背後的 adapter 長什麼樣；adapter 對上層暴露的 port 怎麼切，見 <a href="/blog/programming/go/basic/06-practical/repository-port/" data-link-title="6.6 如何新增 repository port" data-link-desc="先建立儲存邊界，再決定 memory、SQLite 或外部資料庫實作">6.6 如何新增 repository port</a>。</p>
]]></content:encoded></item></channel></rss>