---
title: "資料庫抽換 - SQLite（一） - Clean Architecture in Go"
date: 2025-05-02T00:00:00+08:00
publishDate: 2025-05-02T00:00:00+08:00
lastmod: 2024-10-07T20:05:43+08:00
tags: ["Golang","Clean Architecture","架構","經驗"]
series: "clean-architecture-in-go"
toc: true
permalink: "https://blog.aotoki.me/posts/2025/05/02/clean-architecture-in-go-add-sqlite-1/"
language: "zh-tw"
---


使用 BoltDB（或者 NoSQL）類型的資料庫跟我們原生的 InMemory 模式還是相當接近，那麼利用 Repository 抽象化的特性轉換成 RDBMS（關連式資料庫）是否可行呢？答案是肯定的，我們會使用 [sqlc](https://sqlc.dev/) 來支援 SQLite 或者 PostgreSQL 和 MySQL。

<!--more-->

## sqlc 設定{#configure-sqlc}

sqlc 跟我們前面使用的 Code Generate 工具（如：oapi-codegen、protoc）都是用於生成程式碼為目標，這次我們會讓 sqlc 幫我們根據資料庫的定義生成我們所需的實作，再封裝到 Repository 之中。

在專案根目錄加入 `sqlc.yaml` 並且放入以下內容

```yaml
version: "2"
sql:
  - engine: "sqlite"
    schema: "db/schema.sql"
    queries:
      - "db/tokens.sql"
      - "db/orders.sql"
    gen:
      go:
        package: "sqlite"
        out: "internal/repository/sqlite"
```

這個設定告訴 sqlc 要針對 SQLite 產生相關的實作，並且從 `db/schema.sql` 來偵測資料表的樣式（Schema）以及 `db/tokens.sql` 和 `db/orders.sql` 來取得我們會對資料庫進行操作的方式。

在生成的 Golang 程式碼中，我們選擇生成到 `internal/repository/sqlite` 目錄下，作為 Repository 實作的一個子套件。

## Schema

將 sqlc 設定完畢後，我們需要描述資料表的儲存方式，因此在 `db/schema.sql` 這個檔案寫入以下內容。

```sql
CREATE TABLE IF NOT EXISTS orders (
  id            VARCHAR(36) PRIMARY KEY,
  customer_name VARCHAR(255) NOT NULL
);

CREATE TABLE IF NOT EXISTS order_items (
  id          VARCHAR(36) PRIMARY KEY,
  order_id    VARCHAR(36) NOT NULL,
  name        VARCHAR(255) NOT NULL,
  quantity    INT NOT NULL,
  unit_price  INT NOT NULL,
  FOREIGN KEY (order_id) REFERENCES orders(id)
);

CREATE TABLE IF NOT EXISTS tokens (
  id      VARCHAR(36) PRIMARY KEY,
  data    BLOB NOT NULL,
  version VARCHAR(255) NOT NULL
);
```

跟 NoSQL 不同的地方在於我們無法將 `OrderItem` 直接跟 `Order` 保存在一起，基於資料表正規化的處理，會獨立出另一個單獨的 `order_items` 資料表，作為範例我們只有簡單的設定一個 `FOREIGN KEY` 設定，在實際的應用中可以根據需求調整設計。

除了使用單一的 Schema 檔案外，sqlc 也能支援以 Migration 的方式來生成，以 [goose](https://github.com/pressly/goose) 為例子，我們可以將設定調整為目錄。

```yaml
version: "2"
sql:
  - engine: "sqlite"
    schema: "db/migrations"
    # ...
```

此時只要有類似這樣的檔案結構，sqlc 就能依序偵測資料表的結構，生成正確的 struct 進行對應。

* `0_create_orders.sql`
* `1_create_order_items.sql`
* `2_create_tokens.sql`

> 觸發條件是 `CREATE TABLE` 和 `ALTER TABLE` 只要符合偵測的規則，不使用官方測試過的套件理論上也能很好的運行。

## Queries

使用 sqlc 跟 oapi-codegen 的邏輯很類似，我們都是在描述如何呼叫 API，只是這裡換成對資料庫的查詢是如何進行的。

我們先加入比較簡單的 `db/tokens.sql` 描述讀取、新增 Token 情境的操作。

```sql
-- name: FindToken :one
SELECT * FROM tokens WHERE id = ?
LIMIT 1;

-- name: CreateToken :one
INSERT INTO tokens (
  id, data, version
) VALUES (
  ?, ?, ?
)
RETURNING *;
```

在這裡我們利用 sqlc 可以辨識的註解 `-- name: [name] [type]` 對兩段 SQL 進行標記，第一段是讀取 Token 的動作，我們使用了 `FindToken` 來命名，並且表示只有單一筆資料回傳，因此用 `:one` 進行標記。

因為不考慮更新的情境，所以直接使用了 `INSERT INTO` 來處理，此時使用 `RETURNING *` 表示要回傳一個儲存後的結果，如果用不到我們也可以不額外加入這樣的設定。

接著繼續加入 `db/orders.sql` 的查詢。

```sql
-- name: FindOrder :one
SELECT * FROM orders WHERE id = ?
LIMIT 1;

-- name: ListOrderItems :many
SELECT * FROM order_items
WHERE order_id = ?;

-- name: CreateOrder :one
INSERT INTO orders (
  id, customer_name
) VALUES (
  ?, ?
) RETURNING *;

-- name: CreateOrderItem :one
INSERT INTO order_items (
  id, order_id, name, quantity, unit_price
) VALUES (
  ?, ?, ?, ?, ?
) RETURNING *;
```

基本上跟 `db/tokens.sql` 差不多，因為這次我們需要對兩張資料表進行操作，因此除了 `Order` 本身還需要加入 `OrderItem` 相關的查詢。

因為我們是直接使用 `db/schema.sql` 的方式處理，並且希望直接將資料表維跟 Golang 生成的二進制檔案放在一起，因此還需要額外做 `db/db.go` 這個檔案，用於後續的資料庫初始化，如果是使用其他工具，可以根據需求調整使用方式。

```go
package db

import (
	_ "embed"
)

//go:embed schema.sql
var Schema string
```

到此為止，我們在安裝 `sqlc` 後使用 `sqlc generate` 這個命令產生 SQLite 相關的實作，接下來會說明轉換成 Repository 需要做的處理。

