Hyperledger FabricのState DBをPostgreSQLを使って初期化する

# InitLedgerで既存のRDBからState DBの初期化を実施します
Hyperledger Fabric(以下HF)のDB初期化には色んな方法があると思います。ちなみに、サンプルプログラムで有名なFabcarでは、10レコード分のデータをハードコーディングでState DBへ登録しています:-(
ここでは、レガシーなRDBであるPostgreSQLからデータを読み出し、State DBへ登録する方法を紹介します。
#環境について
動作環境については次の通りです。

- Ubuntu 18.04.4 LTS
- docker-compose 1.26.0
- docker 19.03.11
- HF 2.1.1
- go 1.14.4
- PostgreSQL(Dockerイメージ) 12.3(latest)

#データ構造を考える
これから自動運転の社会になってくると、車は資産として自動車メーカーが管理するのではないかと予想しています。車自体のテクノロジーが高度になりすぎて町工場では対応できないでしょう…
ということで、車の状態を最低限管理できる項目を並べたデータ構造とします。サンプルだし。

```go
type Asset struct {
        Year    string `json:"year"`    // 初度登録年
        Month   string `json:"month"`   // 初度登録月
        Mileage int    `json:"mileage"` // 走行距離(km)
        Battery int    `json:"battery"` // バッテリーライフ(%)
}
type QueryResult struct {
        Key     string `json:"Key"`    // 車台番号
        Record  *Asset
}
```
バッテリーライフは充電状態ではなくバッテリー自体の劣化状態を意味しています。
（初年度登録年・月は製造年・月の方がより良いかもしれません）

# 下準備
下準備をします。PostgreSQLのテーブル定義は端折るのでGoogle先生に聞いてください;-)

### ダミーデータの作成
何はなくともダミーデータ。
1万台分のデータをPostgreSQLに準備します。ダミーデータをでっち上げるgoプログラムを書いてPostgreSQLのDBへ流し込みました。

1. 1万台分のデータをCSVファイルに出力(goプログラム) 
2. PostgreSQLのコンテナへCSVファイルをコピー(/tmpなど)
3. PostgreSQLのコンテナへ入り、`\copy`コマンドでインポート

もちろん、予めPostgreSQLのDBへテーブルを定義しておく必要があります。
CSVファイルの内容は次の通り。

|Key|初年度登録年|初年度登録月|走行距離|バッテリーライフ|
|:-:|:-:|:-:|:-:|:-:|
|"JXT4EGSP0AX100001"|"2010"|"04"|33880|78|
|"JXT5EGSP0AX100002"|"2010"|"02"|6360|96|
|"JXT4EGSP0AX100003"|"2010"|"09"|41040|73|
|:|:|:|:|:

このKey値は、VIN(Vehicle Identification Number)コードと呼ばれる17桁の文字列です。上記はダミーデータなのでもちろん架空の値です。その他の値も`rand`を使って生成しています。初年度登録年はVINと関係してくるのでそれなりのロジックで生成しました。走行距離もそれっぽく。

### chaincodeでPostgreSQLを使えるようにする
goのchaincodeモジュールにPostgreSQLを追加します。
いつからかgoはgo.modというファイルでモジュールを管理しているようです。周回遅れ過ぎです。
当初、ローカルな$GOHOMEでchaincodeを書いてビルドできていたのに、HF上へデプロイするとできないのでハマりました。ローカルな環境では`go get github.com/lib/pq`をしていたのでビルドできていたのです。
HFのchaincode置き場でも同様に`go get github.com/lib/pq`する必要がありました。`asset`という名前でchaincodeを作ったので、ディレクトリ`fabric-samples/chaincode/asset/go`を作ってfabcarから`go.mod`と`go.sum`と`vendor`をコピーしてきます。その際、go.modの`module`を`asset`へ変更します。

```bash
# pwd
/root/fabric/fabric-samples/chaincode/asset/go
# ls
asset.go  go.mod  go.sum vendor
# go get github.com/lib/pq
```

これでgo.modとgo.sumが自動的に更新されます。

```go:go.mod
module asset

go 1.13

require (
        github.com/hyperledger/fabric-contract-api-go v1.1.0
        github.com/lib/pq v1.7.0
)
```

go.modに`github.com/lib/pq v1.7.0`が追加されました。

### PostgreSQLのdocker-composeファイルを作成する
HFのpeerからPostgreSQLのコンテナへアクセスできるようにするcomposeファイルを定義します。
`docker-compose-pgsql.yaml`として`fabric-samples/test-network/docker`ディレクトリへ置きます。
定義内容は次の通り。
1. HFと同じネットワークで稼働させる
2. コンテナ名を定義する(これがホスト名になります)

```yaml:docker-compose-pgsql.yaml
version: '2'

networks:
  test:

services:
  pgsql:
    image: postgres:latest
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - /tank/pgsql-data:/var/lib/postgresql/data
    ports:
      - 5432:5432
    container_name: pgsql
    networks:
      - test
```

`test`というネットワークに`pgsql`という名前でデプロイします。
察しの良い方は気づくかもしれませんが、DBはZFS上に永続化しています。

###fabcarの環境をassetの環境へ書き換える
今回はサンプルコードのfabcar環境を流用しています。

書き換えるファイルは次の2つです。
- fabric-samples/test-network/network.sh
- fabric-samples/test-network/scripts/deployCC.sh

`network.sh`は`fabcar`を`asset`へ文字列置換して、`PostgreSQL`のupとdownの記述を追加します。
`deployCC.sh`は文字列置換のみでOKです。chaincodeのFunction名も必要に応じて変更します※。
※`queryAllCars` -> `QueryAllAssets`みたいな

```bash
# diff network.sh network.sh.org
27c27
<   echo "      - 'deployCC' - deploy the asset chaincode on the channel"
---
>   echo "      - 'deployCC' - deploy the fabcar chaincode on the channel"
244,250d243
<     echo "############################"
<     echo "##### Start PostgreSQL #####"
<     echo "############################"
< 
<     IMAGE_TAG=${PGSQL_IMAGETAG} docker-compose -f $COMPOSE_FILE_PGSQL up -d 2>&1
< 
<     echo
407d399
<   docker-compose -f $COMPOSE_FILE_PGSQL down --volumes --remove-orphans
425c417
<     rm -rf channel-artifacts log.txt asset.tar.gz asset
---
>     rm -rf channel-artifacts log.txt fabcar.tar.gz fabcar
452,453d443
< # docker-compose.yaml file if you are using postgresql
< COMPOSE_FILE_PGSQL=docker/docker-compose-pgsql.yaml
```

作業ディレクトリは、`fabric-samples/fabcar`を`fabric-samples/asset`でコピーします。
中のシェルはchaincodeに依存していないので、そのまま使えます。
- startFabric.sh:HFを開始するシェルです。HFコンテナ群のデプロイからchaincodeのデプロイ、初期化・全件クエリまで自動で実施します。
- networkDown.sh:全てを掃除してなかったことにしてくれます。

`fabcar`ディレクトリには他にもディレクトリがありますが、不要なので削除しても構いません。

# InitLedgerの実装
先ずは必要なライブラリをインポートします。

```go:asset.go
import (
        "bytes"
        "encoding/json"
         :
        "database/sql"
        _ "github.com/lib/pq"
)
```
`"database/sql"`と`_ "github.com/lib/pq"`を追加しています。

```golang:asset.go
func (s *SmartContract) InitLedger(ctx contractapi.TransactionContextInterface) error {
        fmt.Println(time.Now())

        db, err := sql.Open("postgres", "host=pgsql port=5432 user=postgres password=secret dbname=asset sslmode=disable")
        defer db.Close()

        if err != nil {
                return fmt.Errorf("sql.Open: %s", err.Error())
        }

        rows, err := db.Query("SELECT * FROM asset;")

        if err != nil {
                return fmt.Errorf("sql.Query: %s", err.Error())
        }

        var id string
        for rows.Next() {
                var asset Asset
                rows.Scan(&id, &asset.Year, &asset.Month, &asset.Mileage, &asset.Battery)
                assetAsBytes, _ := json.Marshal(asset)
                ctx.GetStub().PutState(id, assetAsBytes)
        }

        fmt.Println(time.Now())
        return nil
}
```
恐ろしくシンプルなコードです。ホスト名・ユーザ名・パスワード、諸々がベタ打ちなのはサンプルなのでご愛嬌。goがわからなくても読むとなんとなく理解できると思います。
1. DBをオープン
2. 全レコードのクエリ(`"SELECT * FROM asset;"`)
3. 1レコードずつ読み取って(`rows.Next()`)、カラムに分解(`rows.Scan`)
4. `rows.Scan`と同時に`Asset`構造体へ設定
5. `json.Marshal`してState DBへ`PutState`

# InitLedgerを実行
1万台分のデータの登録にどのくらいの時間がかかるのでしょう？
実行環境は4C/4TのPentiumにメモリ16GBを積んだマイクロサーバー(LIVA Z)です。
InitLedgerに`time.Now()`をプリントするコードを仕込んであるのでpeerのログを覗いてみます。

```bash
# docker logs 3575482a43e3
2020-06-20 10:05:34.977106747 +0000 UTC m=+5.210858066
2020-06-20 10:05:42.026225952 +0000 UTC m=+12.259977077
```
logsに続く文字列はpeerのコンテナIDです。
State DBへの登録に8秒かかっていますね。1万件なら早いのではないでしょうか。HW環境的にも。
全件クエリにはどのくらいかかるでしょうか。

```bash
# time ./QueryAllAssets.sh
途中省略
"Record":{"year":"2019","month":"09","mileage":2565,"battery":99}},{"Key":"JXT5EGSP0KX100979","Record":{"year":"2019","month":"01","mileage":353,"battery":100}},{"Key":"JXT5EGSP0KX100980","Record":{"year":"2019","month":"10","mileage":7060,"battery":96}},{"Key":"JXT5EGSP0KX100982","Record":{"year":"2019","month":"02","mileage":1806,"battery":99}},{"Key":"JXT5EGSP0KX100984","Record":{"year":"2019","month":"10","mileage":9240,"battery":94}},{"Key":"JXT5EGSP0KX100985","Record":{"year":"2019","month":"03","mileage":36,"battery":100}},{"Key":"JXT5EGSP0KX100991","Record":{"year":"2019","month":"04","mileage":1160,"battery":100}},{"Key":"JXT5EGSP0KX100992","Record":{"year":"2019","month":"09","mileage":6219,"battery":96}},{"Key":"JXT5EGSP0KX100995","Record":{"year":"2019","month":"08","mileage":3792,"battery":98}},{"Key":"JXT5EGSP0KX100996","Record":{"year":"2019","month":"11","mileage":10868,"battery":93}},{"Key":"JXT5EGSP0KX100998","Record":{"year":"2019","month":"09","mileage":5967,"battery":97}}]

real    0m8.896s
user    0m0.175s
sys     0m0.051s
```
画面出力にほとんどの時間を使っています。処理的にはアッと言う間ですね。

比較のために、`peer`コマンドを使ってシェル内ループで1万件を登録してみました。

```bash
# time ./CreateAllAssets.sh
途中省略
2020-06-21 12:33:00.825 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
2020-06-21 12:33:00.951 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
2020-06-21 12:33:01.072 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
2020-06-21 12:33:01.195 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200
2020-06-21 12:33:01.319 JST [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200

real    20m44.886s
user    17m9.930s
sys     3m46.809s
#
```
差は歴然でした。

# 最後に
HFとレガシーRDBとの連携情報があまりなかったので書いてみました。RDBからのReadができればWriteもできそうです。個人情報などはRBDへ保存して都度HFと連携すると良いと思います。また、データ構造にLocation情報も追加して、ブロックチェーンの特徴でもある取引履歴を応用したトレーサビリティ機能が実装できそうです。
RBDとの連携は興味深いのでまた投稿しようと思います。

みなさんのお役に立てたら幸いです。