
資料庫抽換 - SQLite(一) - Clean Architecture in Go
使用 BoltDB(或者 NoSQL)類型的資料庫跟我們原生的 InMemory 模式還是相當接近,那麼利用 Repository 抽象化的特性轉換成 RDBMS(關連式資料庫)是否可行呢?答案是肯定的,我們會使用 sqlc 來支援 SQLite 或者 PostgreSQL 和 MySQL。
sqlc 設定
sqlc 跟我們前面使用的 Code Generate 工具(如:oapi-codegen、protoc)都是用於生成程式碼為目標,這次我們會讓 sqlc 幫我們根據資料庫的定義生成我們所需的實作,再封裝到 Repository 之中。
在專案根目錄加入 sqlc.yaml
並且放入以下內容
1version: "2"
2sql:
3 - engine: "sqlite"
4 schema: "db/schema.sql"
5 queries:
6 - "db/tokens.sql"
7 - "db/orders.sql"
8 gen:
9 go:
10 package: "sqlite"
11 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
這個檔案寫入以下內容。
1CREATE TABLE IF NOT EXISTS orders (
2 id VARCHAR(36) PRIMARY KEY,
3 customer_name VARCHAR(255) NOT NULL
4);
5
6CREATE TABLE IF NOT EXISTS order_items (
7 id VARCHAR(36) PRIMARY KEY,
8 order_id VARCHAR(36) NOT NULL,
9 name VARCHAR(255) NOT NULL,
10 quantity INT NOT NULL,
11 unit_price INT NOT NULL,
12 FOREIGN KEY (order_id) REFERENCES orders(id)
13);
14
15CREATE TABLE IF NOT EXISTS tokens (
16 id VARCHAR(36) PRIMARY KEY,
17 data BLOB NOT NULL,
18 version VARCHAR(255) NOT NULL
19);
跟 NoSQL 不同的地方在於我們無法將 OrderItem
直接跟 Order
保存在一起,基於資料表正規化的處理,會獨立出另一個單獨的 order_items
資料表,作為範例我們只有簡單的設定一個 FOREIGN KEY
設定,在實際的應用中可以根據需求調整設計。
除了使用單一的 Schema 檔案外,sqlc 也能支援以 Migration 的方式來生成,以 goose 為例子,我們可以將設定調整為目錄。
1version: "2"
2sql:
3 - engine: "sqlite"
4 schema: "db/migrations"
5 # ...
此時只要有類似這樣的檔案結構,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 情境的操作。
1-- name: FindToken :one
2SELECT * FROM tokens WHERE id = ?
3LIMIT 1;
4
5-- name: CreateToken :one
6INSERT INTO tokens (
7 id, data, version
8) VALUES (
9 ?, ?, ?
10)
11RETURNING *;
在這裡我們利用 sqlc 可以辨識的註解 -- name: [name] [type]
對兩段 SQL 進行標記,第一段是讀取 Token 的動作,我們使用了 FindToken
來命名,並且表示只有單一筆資料回傳,因此用 :one
進行標記。
因為不考慮更新的情境,所以直接使用了 INSERT INTO
來處理,此時使用 RETURNING *
表示要回傳一個儲存後的結果,如果用不到我們也可以不額外加入這樣的設定。
接著繼續加入 db/orders.sql
的查詢。
1-- name: FindOrder :one
2SELECT * FROM orders WHERE id = ?
3LIMIT 1;
4
5-- name: ListOrderItems :many
6SELECT * FROM order_items
7WHERE order_id = ?;
8
9-- name: CreateOrder :one
10INSERT INTO orders (
11 id, customer_name
12) VALUES (
13 ?, ?
14) RETURNING *;
15
16-- name: CreateOrderItem :one
17INSERT INTO order_items (
18 id, order_id, name, quantity, unit_price
19) VALUES (
20 ?, ?, ?, ?, ?
21) RETURNING *;
基本上跟 db/tokens.sql
差不多,因為這次我們需要對兩張資料表進行操作,因此除了 Order
本身還需要加入 OrderItem
相關的查詢。
因為我們是直接使用 db/schema.sql
的方式處理,並且希望直接將資料表維跟 Golang 生成的二進制檔案放在一起,因此還需要額外做 db/db.go
這個檔案,用於後續的資料庫初始化,如果是使用其他工具,可以根據需求調整使用方式。
1package db
2
3import (
4 _ "embed"
5)
6
7//go:embed schema.sql
8var Schema string
到此為止,我們在安裝 sqlc
後使用 sqlc generate
這個命令產生 SQLite 相關的實作,接下來會說明轉換成 Repository 需要做的處理。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - Clean Architecture in Go
- 目標設定 - Clean Architecture in Go
- wire 的依賴注入 - Clean Architecture in Go
- 案例說明 - Clean Architecture in Go
- 操作介面設計 - Clean Architecture in Go
- Place Order 實作 Controller 部分 - Clean Architecture in Go
- Place Order 實作 Entity 部分 - Clean Architecture in Go
- Place Order 實作 Repository 部分 - Clean Architecture in Go
- Lookup Order 功能 - Clean Architecture in Go
- Tokenization 機制設計 - Clean Architecture in Go
- 在 Place Order 實作 Token 機制 - Clean Architecture in Go
- 在 Lookup Order 實作 Token 機制 - Clean Architecture in Go
- Token 內容加密 - Clean Architecture in Go
- gRPC Server 準備 - Clean Architecture in Go
- gRPC Server 實作 - Clean Architecture in Go
- 輸入檢查 - Clean Architecture in Go
- 資料庫抽換 - BoltDB - Clean Architecture in Go
- 資料庫抽換 - SQLite(一) - Clean Architecture in Go