
資料庫抽換 - BoltDB - Clean Architecture in Go
當 Clean Architecture 的實踐完善後,我們在各種功能的抽換就能獲得許多彈性,接下來我們會以 BoltDB 來示範將原本儲存在記憶體的資訊,轉換為保存在硬碟的持久性版本。
TokenRepository
TokenRepository 相對簡單一些,我們可以在 internal/repository
下增加一個 bolt_tokens.go
的檔案,用於實作 BoltDB 版本的 TokenRepository。
1// ...
2
3const BoltTokensTableName = "tokens"
4
5type BoltTokenSchema struct {
6 Id string
7 Data []byte
8 Version string
9}
10
11type BoltTokenRepository struct {
12 cipher cipher.Block
13 db *bolt.DB
14 tableName string
15}
16
17func NewBoltTokenRepository(db *bolt.DB) (*BoltTokenRepository, error) {
18 cipher, err := aes.NewCipher([]byte(tokenEncryptionKey))
19 if err != nil {
20 return nil, err
21 }
22
23 return &BoltTokenRepository{
24 cipher: cipher,
25 db: db,
26 tableName: BoltTokensTableName,
27 }, nil
28}
29
30func (r *BoltTokenRepository) Find(ctx context.Context, tokenStr string) (*tokens.Token, error) {
31 segments := strings.SplitN(tokenStr, ":", 2)
32 if len(segments) != 2 {
33 return nil, tokens.ErrTokenNotFound
34 }
35
36 id := segments[1]
37 var token BoltTokenSchema
38 err := r.db.View(func(tx *bolt.Tx) error {
39 bucket := tx.Bucket([]byte(r.tableName))
40 if bucket == nil {
41 return tokens.ErrTokenNotFound
42 }
43
44 boltValue := bucket.Get([]byte(id))
45 if boltValue == nil {
46 return tokens.ErrTokenNotFound
47 }
48
49 return json.Unmarshal(boltValue, &token)
50 })
51
52 if err != nil {
53 return nil, err
54 }
55
56 rawData, err := decrypt(r.cipher, token.Data)
57 if err != nil {
58 return nil, tokens.ErrUnableToDecrypt
59 }
60
61 return tokens.New(
62 token.Id,
63 tokens.WithVersion(token.Version),
64 tokens.WithData(rawData),
65 ), nil
66}
67
68func (r *BoltTokenRepository) Save(ctx context.Context, token *tokens.Token) error {
69 encryptedData, err := encrypt(r.cipher, token.Data())
70 if err != nil {
71 return tokens.ErrUnableToEncrypt
72 }
73
74 return r.db.Update(func(tx *bolt.Tx) error {
75 bucket, err := tx.CreateBucketIfNotExists([]byte(r.tableName))
76 if err != nil {
77 return err
78 }
79
80 tokenSchema := BoltTokenSchema{
81 Id: token.Id(),
82 Data: encryptedData,
83 Version: token.Version(),
84 }
85
86 data, err := json.Marshal(tokenSchema)
87 if err != nil {
88 return err
89 }
90
91 return bucket.Put([]byte(token.Id()), data)
92 })
93}
BoltDB 版本的實作跟 InMemory 版本基本上非常相似,因為都是採用 Key-Value 的機制儲存,唯一不同的地方在於我們將原本的 map[string]InMemoryTokenSchema
替換成 *bolt.DB
用於呼叫資料庫底層。
除此之外,還增加了 tableName
這個欄位,用於記錄 Bucket 的名稱,這是 BoltDB 中用來表示一系列 Key-Value 組合的單位,剛好跟 map[string]InMemoryTokenSchema
的意義是接近的。
因為 BoltDB 的 Value 是以 []byte
的方式記錄,我們無法直接用 struct
來保存,此時可以選擇像是 gob
(Golang 內建的序列化格式)json
甚至 protobuf
這類能轉換成 []byte
資料的方式,這個範例選擇以 json
來進行序列化。
不延用
InMemoryTokenSchema
另外定義BoltTokenSchema
的原因是區分不同資料的保存方式,假設 Schema 都非常一致的話,使用統一的TokenSchema
也是個不錯的做法。
OrderRepository
有了 TokenRepository 的案例後,OrderRepository 基本上就是用相同的方式實作一遍即可,可以參考以下 bolt_orders.go
的範例程式碼。
1const BoltOrdersTableName = "orders"
2
3type BoltOrderItemSchema struct {
4 Name string
5 Quantity int
6 UnitPrice int
7}
8
9type BoltOrderSchema struct {
10 Id string
11 CustomerName string
12 Items []BoltOrderItemSchema
13}
14
15type BoltOrderRepository struct {
16 db *bolt.DB
17 tableName string
18}
19
20func NewBoltOrderRepository(db *bolt.DB) *BoltOrderRepository {
21 return &BoltOrderRepository{
22 db: db,
23 tableName: BoltOrdersTableName,
24 }
25}
26
27func (r *BoltOrderRepository) Find(ctx context.Context, id string) (*orders.Order, error) {
28 var order BoltOrderSchema
29 err := r.db.View(func(tx *bolt.Tx) error {
30 bucket := tx.Bucket([]byte(r.tableName))
31 if bucket == nil {
32 return orders.ErrOrderNotFound
33 }
34
35 boltValue := bucket.Get([]byte(id))
36 if boltValue == nil {
37 return orders.ErrOrderNotFound
38 }
39
40 return json.Unmarshal(boltValue, &order)
41 })
42
43 if err != nil {
44 return nil, err
45 }
46
47 orderEntity := orders.New(order.Id, order.CustomerName)
48 for _, item := range order.Items {
49 err := orderEntity.AddItem(item.Name, item.Quantity, item.UnitPrice)
50 if err != nil {
51 return nil, err
52 }
53 }
54
55 return orderEntity, nil
56}
57
58func (r *BoltOrderRepository) Save(ctx context.Context, order *orders.Order) error {
59 boltOrder := BoltOrderSchema{
60 Id: order.Id(),
61 CustomerName: order.CustomerName(),
62 Items: []BoltOrderItemSchema{},
63 }
64
65 for _, item := range order.Items() {
66 boltOrder.Items = append(boltOrder.Items, BoltOrderItemSchema{
67 Name: item.Name(),
68 Quantity: item.Quantity(),
69 UnitPrice: item.UnitPrice(),
70 })
71 }
72
73 return r.db.Update(func(tx *bolt.Tx) error {
74 bucket, err := tx.CreateBucketIfNotExists([]byte(r.tableName))
75 if err != nil {
76 return err
77 }
78
79 boltValue, err := json.Marshal(boltOrder)
80 if err != nil {
81 return err
82 }
83
84 return bucket.Put([]byte(order.Id()), boltValue)
85 })
86}
基本上只有 Schema 的不同,以及產生 Entity 方式的差異,其他基本上都是相同的做法。
切換資料庫
因為我們現在允許兩種不同的資料庫存在,因此需要可以進行切換的選項,我們可以利用 Golang 的標準函式庫 flag
來做到這點,以下以 gRPC 版本的 main.go
作為範例。
1// ...
2func main() {
3 databaseType := flag.String("database", "in-memory", "Database type to use")
4 flag.Parse()
5
6 server, cleanup, err := initialize(*databaseType)
7 if err != nil {
8 log.Fatalf("Error initializing handler: %v", err)
9 }
10 defer cleanup()
11
12 if err := server.Serve(); err != nil {
13 log.Fatalf("Error starting server: %v", err)
14 }
15}
因為我們預期 wire
回傳的內容都是相同的,因此只需要使用一個 initialize()
來進行判斷即可,除此之外還增加了 cleanup()
方法的回傳,用於處理 BoltDB 的關閉。
同樣在 main.go
加入 provideBoltDb()
方法,用於建立資料庫。
1// ...
2func provideBoltDb() (*bolt.DB, func(), error) {
3 db, err := bolt.Open("bolt.db", 0600, nil)
4 if err != nil {
5 return nil, nil, err
6 }
7
8 return db, func() { db.Close() }, nil
9}
10// ...
除了放在 main.go
之外,也可以考慮放到 internal/repository
之類的位置,讓依賴注入可以提供 *config.Config
這類物件,用於切換 bolt.db
的路徑,作為範例我們先以比較簡單的方式進行。
最後更新 wire.go
調整為能跟依照 databaseType
提供的類型切換的版本。
1// ...
2
3func initialize(databaseType string) (*grpc.Server, func(), error) {
4 switch databaseType {
5 case "in-memory":
6 return initializeInMemory()
7 case "bolt":
8 return initializeBolt()
9 default:
10 return nil, func() {}, errors.New("unsupported database type")
11 }
12}
13
14func initializeInMemory() (*grpc.Server, func(), error) {
15 wire.Build(
16 repository.DefaultSet,
17 usecase.DefaultSet,
18 validator.DefaultSet,
19 grpc.DefaultSet,
20 )
21
22 return nil, nil, nil
23}
24
25func initializeBolt() (*grpc.Server, func(), error) {
26 wire.Build(
27 provideBoltDb,
28 repository.BoltSet,
29 usecase.DefaultSet,
30 validator.DefaultSet,
31 grpc.DefaultSet,
32 )
33
34 return nil, nil, nil
35}
基本上只需要根據 in-memory
和 bolt
選擇不同的 initialize()
方法,以及提供不一樣 wire.Build
組合來達成。
這個方法跟
wire
的範例是相同的做法,然而理論上我們也可以單獨針對 Repository 的部分做切換,但可能會有不少額外的複雜度,可以根據狀況評估使用。
此時使用 go run ./cmd/grpc -database bolt
先做 Place Order 後關閉伺服器,使用拿到的 ID 再次用 Lookup Order 查詢,就不會和 InMemory 版本一樣無法找到。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- 連載介紹 - 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