こんにちは。KOUKIです。
今日は、Interfaceの使い所の一つである「DBの切り替え」について記事にしました。
アプリケーションを実装していく中で、開発中はDynamoDBを使っていたのに、本番ではRedisを使うことになっちゃった!といった経験はありませんか?
そんな時に使えるテクニックです。
<目次>
進め方
DBの切り替えと書きましたが、今回はDynamoDBとsyncパッケージのMapを使ったDemoアプリケーションを作成します。Mapは別にデータベースじゃないじゃん!?と思うかもしれませんが、MySQLでもPostgreSQLでも問題なく動きますので、ご心配なく。
実装としては、最初にDynamoDBの接続処理を書き、次にsync.Mapを使ったローカルキャッシュを作成します。そして最後に、Interfaceを使ってDBの切り替え処理を実装する流れです。
環境
- Mac
- Docker 及び docker-compose
Dockerとdocker-composeを使いますが、かなり便利なツールです。この機会に使ってみてはいかがでしょうか。
Workspace
最初にワークスペースを用意しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
touch Dockerfile touch docker-compose.yml touch main.go go mod init interface-demo mkdir data mkdir repository mkdir repository/mock touch repository/dynamodb.go touch repository/mock/dynamodb.go mkdir domain touch domain/dynamodb.go touch mock.json tree . ├── Dockerfile ├── data │ └── shared-local-instance.db ├── docker-compose.yml ├── domain │ └── dynamodb.go ├── go.mod ├── main.go ├── mock.json └── repository ├── dynamodb.go └── mock └── dynamodb.go |
dataディレクトリには、dynamodbのデータが入ります
事前準備
たくさんのファイルが必要ですが、コピペでOKです。
Dockerfile
Dockerfileには、Go言語の実行環境を定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
FROM golang:1.15-alpine3.12 RUN apk update && \ apk upgrade && \ apk add git RUN go get github.com/cespare/reflex ENV CGO_ENABLED=0 WORKDIR /go/src/app COPY go.* main.go ./ RUN go mod download |
「github.com/cespare/reflex」は、ソースコードのビルドを自動的に行ってくれる便利ツールです。これを導入することでDockerコンテナを立ち上げ直さなくとも、ファイルを保存するだけでソースコードの変更を反映させることが可能です。
ただし、Windowsだと動かないかもしれません。
docker-compose.yml
ここには、GoとDynamoDBのコンテナ情報を記載します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
version: "3" services: golang: build: . ports: - 80:8080 volumes: - .:/go/src/app/ command: > sh -c "reflex -s -r '\.go$$' go run main.go" dynamodb: image: amazon/dynamodb-local ports: - 8000:8000 volumes: - ./data:/home/dynamodblocal/data command: -jar DynamoDBLocal.jar -dbPath ./data -sharedDb |
main.go
main.goには、net/httpパッケージを使った簡易サーバーを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package main import ( "fmt" "log" "net/http" ) func main() { http.HandleFunc("/", helloHandler) log.Fatal(http.ListenAndServe(":8080", nil)) } func helloHandler(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, ` <h1>How to use Interface?</h1> <h2>Hello from this Golang app!</h2> <p>This is new!</p>`) } |
コンテナの立ち上げ
以下のコマンドで、コンテナを立ち上げましょう。
1 |
docker-compose up |
動作確認
コンテナが立ち上がったら、「localhost」にブラウザからアクセスしてください。

上の画像のように表示されていればOKです。
DynamoDBの実装
では、DynamoDBの接続処理を実装していきましょう。
DynamoDBの使い方については、以下を参考にしてください。
まずは、必要なデータ型を定義します。
1 2 3 4 5 6 7 |
// domain/dynamodb.go package domain type Data struct { Name string `dynamo:"name,hash"` Text string `dynamo:"text"` } |
「dynamo」キーは、DynamoDBのデータとして扱えるようにするもので必須です。また、定義したデータ型の中に「hash」がないとDynamoテーブルを作成するときにエラーになるので注意しましょう。
接続処理は、以下のような感じで書きました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// repository/dynamodb.go package repository import ( "interface-demo/domain" "github.com/guregu/dynamo" ) type DynamoDBRepo struct { dynamoDB *dynamo.DB tableName, primaryKey string } func NewDynamoDBRepo(d *dynamo.DB, isMock bool) *DynamoDBRepo { if isMock { return nil } return &DynamoDBRepo{ dynamoDB: d, tableName: "test_list", primaryKey: "name", } } // Dataを格納 func (d *DynamoDBRepo) Store(data domain.Data) error { if err := d.dynamoDB.Table(d.tableName).Put(data).Run(); err != nil { return err } return nil } // Dataを取得 func (d *DynamoDBRepo) GetByName(name string) (domain.Data, error) { var data domain.Data if err := d.dynamoDB.Table(d.tableName).Get(d.primaryKey, name).One(&data); err != nil { return domain.Data{}, err } return data, nil } |
次は、main関数からこの処理を呼びます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
package main import ( "fmt" "interface-demo/domain" "interface-demo/repository" "log" "net/http" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/guregu/dynamo" ) var ( dr *repository.DynamoDBRepo ) func logFatal(err error) { if err != nil { log.Fatal(err) } } func main() { // dynamodbの基本設定 cfgs := aws.NewConfig() cfgs.WithEndpoint("http://dynamodb:8000") cfgs.WithRegion("ap-northeast-1") cfgs.WithCredentials(credentials.NewStaticCredentials("dummy", "dummy", "dymmy")) sess, err := session.NewSession(cfgs) logFatal(err) dynamodb := dynamo.New(session.Must(sess, err)) dynamodb.CreateTable("test_list", domain.Data{}).Run() dr = repository.NewDynamoDBRepo(dynamodb, false) http.HandleFunc("/", requestHandler) log.Fatal(http.ListenAndServe(":8080", nil)) } func requestHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { // http://localhost?name=test => testが取得できる name := r.FormValue("name") data, err := dr.GetByName(name) if err == nil { message := fmt.Sprintf(` <h1>Name: %s</h1> <h3>Text: %s</h3>`, data.Name, data.Text, ) fmt.Fprintf(w, message) } else { fmt.Fprintf(w, ` <h1>No Item!</h1>`) } } else if r.Method == "POST" { data := domain.Data{ Name: r.FormValue("name"), Text: r.FormValue("text"), } err := dr.Store(data) logFatal(err) fmt.Fprintf(w, ` <h1>Success Store!</h1>`) } else { fmt.Fprintf(w, ` <h1>Not Allowed Method</h1>`) } } |
このアプリは、以下のリクエストを送ることで動作します。
POST: http://localhost?name=test&text=test2
GET: http://localhost?name=test


ローカルキャッシュの実装
sync.Mapを使って、ローカルキャッシュを作成します。これは、DynamoDBと切り替えができるようにするためのもので、他のDBでも構いません。
ローカルキャッシュについては、こちらを参考にしてください。
まずは、データ格納処理です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// repository/mock/dynamodb.go package mock import ( "errors" "interface-demo/domain" "sync" ) type mockDynamoDBRepo struct { mockDB sync.Map } func NewMockDynamoDBRepo() mockDynamoDBRepo { return mockDynamoDBRepo{} } // Dataを格納 func (d *mockDynamoDBRepo) Store(data domain.Data) error { d.mockDB.Store(data.Name, data.Text) return nil } // Dataを取得 func (d *mockDynamoDBRepo) GetByName(name string) (domain.Data, error) { text, ok := d.mockDB.Load(name) if !ok { return domain.Data{}, errors.New("No Item") } return domain.Data{Name: name, Text: text.(string)}, nil } |
Interfaceを使ったDB切り替え
いよいよ、Interfaceを使ってDB切り替え処理を実装します。
やりたいことは、先ほど作成したDynamoDBとローカルキャッシュをisMock変数に格納される値によって切り替えることです。
パラメータやコンフィグ情報によってDBを切り替えられたら便利ですよね。
DomainにInterfaceを定義
最初にDomainにInterfaceを定義します。
1 2 3 4 5 6 7 8 9 10 11 |
// domain/dynamodb.go package domain type Data struct {...} // Mock化するためにInterfaceを定義 type DynamoDBRepo interface { Store(data Data) error GetByName(name string) (Data, error) } |
Interfaceに設定するメソッドは、DynamoDBとローカルキャッシュに設定したメソッドと同じセマフォでなければなりません。これが鍵です。
Interfaceの反映
repository/dynamodb.goを以下のように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
// repository/dynamodb.go package repository import ( "interface-demo/domain" "interface-demo/repository/mock" "github.com/guregu/dynamo" ) // DynamoDBRepo -> dynamoDBRepoに変更 type dynamoDBRepo struct { dynamoDB *dynamo.DB tableName, primaryKey string } // 戻り値をDynamoDBRepoインターフェースに変更 func NewDynamoDBRepo(d *dynamo.DB, isMock bool) domain.DynamoDBRepo { if isMock { // Mockのレポジトリを返す return mock.NewMockDynamoDBRepo() } return &dynamoDBRepo{ dynamoDB: d, tableName: "test_list", primaryKey: "name", } } // Dataを格納 func (d *dynamoDBRepo) Store(data domain.Data) error { if err := d.dynamoDB.Table(d.tableName).Put(data).Run(); err != nil { return err } return nil } // Dataを取得 func (d *dynamoDBRepo) GetByName(name string) (domain.Data, error) { var data domain.Data if err := d.dynamoDB.Table(d.tableName).Get(d.primaryKey, name).One(&data); err != nil { return domain.Data{}, err } return data, nil } |
NewDynamoDBRepoの戻り値をInterfaceにしました。
また、mockのNewMockDynamoDBRepoに関しても戻り値をInterfaceにします。
1 2 3 4 |
// repository/mock/dynamodb.g func NewMockDynamoDBRepo() domain.DynamoDBRepo { return &mockDynamoDBRepo{} } |
Go言語では、Interfaceに定義したメソッドを実装していれば、暗黙的に実装したことにしてくれます。「mockDynamoDBRepo」も「dynamoDBRepo」もどちらもStoreとGetByNameメソッドを定義しているので、インターフェース要件を満たし、戻り値として返すことができるのです。
切り替え処理
main関数に切り替え処理を追加しましょう。
mock.jsonに以下の記述を書いてください。
1 2 3 |
{ "is_mock": true } |
これは、Mockモードにするか否かの設定値です。
main関数を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
package main ... var ( // Interfaceに変更 dr domain.DynamoDBRepo // isMockを外だし isMock bool ) ... func init() { // 設定ファイルを読み込む viper.SetConfigFile(`mock.json`) err := viper.ReadInConfig() logFatal(err) isMock = viper.GetBool("is_mock") if isMock { log.Println("####### START Mock Mode ######") } else { log.Println("####### START DynamoDB Mode ######") } } func main() { // dynamodbの基本設定 ... // isMockを渡す dr = repository.NewDynamoDBRepo(dynamodb, isMock) ... } |
これでOKです。dr変数には、domainに定義したDynamoDBRepo Interfaceを指定しました。これで、isMockの値によってNewMockDynamoDBRepoかdynamoDBRepoのどちらかを取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 |
// repository/dynamodb.go func NewDynamoDBRepo(d *dynamo.DB, isMock bool) domain.DynamoDBRepo { if isMock { // mockDynamoDBRepoを返す return mock.NewMockDynamoDBRepo() } return &dynamoDBRepo{ // dynamoDBRepoを返す dynamoDB: d, tableName: "test_list", primaryKey: "name", } } |
これにより、それぞれのデータに紐づいたメソッドが呼ばれるので、DBの切り替え処理が実現できるという仕組みです。
動作確認
動作確認をしてみましょう。
Mockモード
以下のURLにリクエストを送ってみましょう。
POST: http://localhost?name=mock&text=This is a MockMode
GET: http://localhost?name=mock



ローカルキャッシュからデータの格納/取得ができたようですね。
DynamoDBモード
is_mockの設定をfalseにします。
1 2 3 4 |
{ "is_mock": false } |
コンテナを立ち上げ直します。
1 2 3 4 5 |
# Ctrl + c でコンテナを止める # コンテナ立ち上げ docker-compose up golang_1 | [00] 2021/01/26 08:52:31 ####### START DynamoDB Mode ###### |
POST: http://localhost?name=dynamodb&text=This is a dynamodb
GET: http://localhost?name=dynamodb



OKですね。うまく切り替えられたようです。
一応、DynamoDBのデータも確認してみましょう。「localhost:8000/shell」にアクセスしてください。

Mockのデータが確認できないので、切り替え処理には問題がないようですね。
おわりに
いかがだったでしょうか。
Interfaceを使えばこんなことも実装可能です。参考になれば嬉しいですね。
それでは、また!
ソースコード
main.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
package main import ( "fmt" "interface-demo/domain" "interface-demo/repository" "log" "net/http" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/session" "github.com/guregu/dynamo" "github.com/spf13/viper" ) var ( // Interfaceに変更 dr domain.DynamoDBRepo // isMockを外だし isMock bool ) func logFatal(err error) { if err != nil { log.Fatal(err) } } func init() { // 設定ファイルを読み込む viper.SetConfigFile(`mock.json`) err := viper.ReadInConfig() logFatal(err) isMock = viper.GetBool("is_mock") if isMock { log.Println("####### START Mock Mode ######") } else { log.Println("####### START DynamoDB Mode ######") } } func main() { // dynamodbの基本設定 cfgs := aws.NewConfig() cfgs.WithEndpoint("http://dynamodb:8000") cfgs.WithRegion("ap-northeast-1") cfgs.WithCredentials(credentials.NewStaticCredentials("dummy", "dummy", "dymmy")) sess, err := session.NewSession(cfgs) logFatal(err) dynamodb := dynamo.New(session.Must(sess, err)) dynamodb.CreateTable("test_list", domain.Data{}).Run() // isMockを渡す dr = repository.NewDynamoDBRepo(dynamodb, isMock) http.HandleFunc("/", requestHandler) log.Fatal(http.ListenAndServe(":8080", nil)) } func requestHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { // http://localhost?name=test => testが取得できる name := r.FormValue("name") data, err := dr.GetByName(name) if err == nil { message := fmt.Sprintf(` <h1>Name: %s</h1> <h3>Text: %s</h3>`, data.Name, data.Text, ) fmt.Fprintf(w, message) } else { fmt.Fprintf(w, ` <h1>No Item!</h1>`) } } else if r.Method == "POST" { data := domain.Data{ Name: r.FormValue("name"), Text: r.FormValue("text"), } err := dr.Store(data) logFatal(err) fmt.Fprintf(w, ` <h1>Success Store!</h1>`) } else { fmt.Fprintf(w, ` <h1>Not Allowed Method</h1>`) } } |
domain/dynamodb.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// domain/dynamodb.go package domain type Data struct { Name string `dynamo:"name,hash"` Text string `dynamo:"text"` } // Mock化するためにInterfaceを定義 type DynamoDBRepo interface { Store(data Data) error GetByName(name string) (Data, error) } |
repository/mock/dynamodb.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
// repository/mock/dynamodb.go package mock import ( "errors" "interface-demo/domain" "sync" ) type mockDynamoDBRepo struct { mockDB sync.Map } func NewMockDynamoDBRepo() domain.DynamoDBRepo { return &mockDynamoDBRepo{} } // Dataを格納 func (d *mockDynamoDBRepo) Store(data domain.Data) error { d.mockDB.Store(data.Name, data.Text) return nil } // Dataを取得 func (d *mockDynamoDBRepo) GetByName(name string) (domain.Data, error) { text, ok := d.mockDB.Load(name) if !ok { return domain.Data{}, errors.New("No Item") } return domain.Data{Name: name, Text: text.(string)}, nil } |
repository/dynamodb.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
// repository/dynamodb.go package repository import ( "interface-demo/domain" "interface-demo/repository/mock" "github.com/guregu/dynamo" ) // DynamoDBRepo -> dynamoDBRepoに変更 type dynamoDBRepo struct { dynamoDB *dynamo.DB tableName, primaryKey string } // 戻り値をDynamoDBRepoインターフェースに変更 func NewDynamoDBRepo(d *dynamo.DB, isMock bool) domain.DynamoDBRepo { if isMock { // Mockのレポジトリを返す return mock.NewMockDynamoDBRepo() } return &dynamoDBRepo{ dynamoDB: d, tableName: "test_list", primaryKey: "name", } } // Dataを格納 func (d *dynamoDBRepo) Store(data domain.Data) error { if err := d.dynamoDB.Table(d.tableName).Put(data).Run(); err != nil { return err } return nil } // Dataを取得 func (d *dynamoDBRepo) GetByName(name string) (domain.Data, error) { var data domain.Data if err := d.dynamoDB.Table(d.tableName).Get(d.primaryKey, name).One(&data); err != nil { return domain.Data{}, err } return data, nil } |
コメントを残す
コメントを投稿するにはログインしてください。