Bigtableのデータ構造を理解しGoで読み書きする【図あり】



この記事は [CyberAgent 22 新卒 Advent Clendar 2021](https://adventar.org/calendars/6671) 16日目の記事です。

# 概要
現在内定者バイトを行なっている部署でBigtableを使用する頻度が高かったので、今回は少しクセのあるBigtableのデータ構造とGolangでのBigtableのデータの読み書きについてまとめました。

# そもそもBigtableとは
BigtableとはGCPのフルマネージドでスケーラブルな NoSQL データベース サービスです。
超大容量データをKey-Valueストアに格納するのに長けていたり、低レイテンシで高スループットという特徴があります。
もっと詳しく知りたい方はしたの公式ドキュメントを参照してください。

### 公式Document
https://cloud.google.com/bigtable/docs/overview
# データ構造

データ構造ですが大きな構成要素として

- **RowKey(行キー)**, 

- **ColumnQualifier(列修飾子)**
- **ColumnFamily(列ファミリー)**
- **Timestamp(タイムスタンプ)**
- **Value(バリュー)**

で構成されています。

少しイメージがつきにくいと思うので今回はとある**お気に入りリスト**を例にして図を使って理解を深めていきましょう。

以下設計です。

##### お気に入りリストテーブル(favorite)
| 行キー | 列ファミリー:列修飾子 | タイムスタンプ | 値 |
| --- | --- | --- | --- |
| UserID | value:`<ContentID>` | 0 | `<FavoriteInfo>` |
| UserID | addedAt:`<AddedAt>` | 0 | `<FavoriteInfo>` |

- `<ContentID>`はお気に入りコンテンツのIDです。Stringです。
- `<AddedAt>`はお気に入り情報の追加日時です
- `<FavoriteInfo>`はお気に入り情報です。構造体です。
- 今回タイムスタンプは簡略化のため0で設定しています。

テーブル設計自体に関しては扱うデータによっていろいろなやり方があるので気になる方は以下のリンクを参考にしてみてください。

https://christina04.hatenablog.com/entry/how-to-save-time-series-data-in-bigtable


上記のテーブル設計で図解してみると下の図のようになります。

注: 図解では`<FavoriteInfo>`構造体の中身の記述は省略し連番を追加し表記してます。

![スクリーンショット 2021-12-16 11.21.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/532470/09f1dfd5-a1ee-0172-720f-f2e137dcc868.png)


# GoによるBigtableの読み書きについて
公式のGo用GoogleCloudクライアントライブラリのCloudBigtableパッケージを使用して、Bigtableへの接続やテーブルの作成・削除、データの読み書きを行うことができます

https://pkg.go.dev/cloud.google.com/go/bigtable

今回はBigtableへの接続やテーブルの作成・削除などの説明は省略して実装上でより重要な**書き込み**と**読み込み**について少し踏み込んでまとめていこうと思います。

省略部分に関しては以下の公式のチュートリアル記事を参考にしてください。

https://cloud.google.com/bigtable/docs/samples-go-hello


## 書き込み
#### 行キー(単体)での書き込み

```golang:go
    mut := bigtable.NewMutation()
	value, err := json.Marshal(item)
	if err != nil {
		log.Error("bigtable: failed to marshal json", log.Ferror(err))
		return err
	}
	ts := bigtable.Timestamp(0)
	mut.Set(columnFamilyName, columnQualifierNameValue, ts, value)
	if err := table.Apply(ctx, rowKey, mut); err != nil {
		log.Error("bigtable: failed to apply item", log.Ferror(err))
		return err
	}
```

基本的には`bigtable.NewMutation()`でMutationを作成し、`Set()`でカラムファミリーやカラム修飾子、タイムスタンプ、バリューをセットし、`Apply()`で行キーを指定し書き込みます。

同じ行キーを持った値を複数保存したい場合であれば複数回`Set()`を使用してApplyで一気に書き込みましょう。
#### 複数行の書き込み

こちらは複数のItemというデータを保存する架空の関数を例にします

```golang:go
     
func SaveItem(ctx context.Context, items []model.Item) error {
	ts := bigtable.Timestamp(0)
	muts := make([]*bigtable.Mutation, len(items))
	rowKeys := make([]string, len(items))

	for i := range items {
		mut := bigtable.NewMutation()
		value, err := items[i].Marshal()
		if err != nil {
			return code.Errorf(code.Unexpected, "failed to marshal item: %s", err)
		}
		mut.Set(columnFamilyName, columnQualifierName, ts, value)
		muts[i] = mut
		rowKeys[i] = p.rowKey(items[i].ID)
	}

	if _, err := p.table.ApplyBulk(ctx, rowKeys, muts); err != nil {
		return code.Errorf(code.Bigtable, "failed to apply bulk items: %s", err)
	}

	return nil
}

```
複数の行を保存したい場合は`ApplyBulk()`を使用します。

## 読み込み
####  行キー(単体)での読み込み
`Table.ReadRow()`で1つの行キーを使用して、行を直接取得します。第3引数ではよくFilterを使用します。基本的には`bigtable.RowFilter()`の引数にbigtable.Filterを返す関数を入れて使います。



```go:golang
row, err := tbl.ReadRow(
		context.Background(),
		rowKey,
		bigtable.RowFilter(bigtable.ChainFilters(
			bigtable.FamilyFilter(familyColumnName),
			bigtable.ColumnFilter(ColumnQualifierName),
			bigtable.LatestNFilter(1),
		)),
	)
if err != nil {
        log.Fatalf("Could not read row with key %s: %v", rowKey, err)
}

```


####  行キー(単体)での読み込み 
こちらは複数のItemというデータを取得する架空の関数を例にします

```golang:golang
func GetItemsFromBigtable(ctx context.Context, table bigtable.Table, rowKeys []string, columnFamilyName, columnQualifierName string) ([]model.Item, error) {       
	ret := make([]model.Item, 0, len(rowKeys*100))
        

	err := s.table.ReadRows(
        ctx, 
        bigtable.RowList(rowKeys),
		func(row bigtable.Row) bool {
			var item model.Item
			if err := item.Unmarshal(row[columnFamilyName][0].Value); err != nil {
				return false
			}

			ret = append(ret, item)
			return true
		},
		bigtable.RowFilter(bigtable.ChainFilters(
			bigtable.FamilyFilter(columnFamilyName),
			bigtable.ColumnFilter(columnQualifierName),
			bigtable.LatestNFilter(1),
		)),
	)
	if err != nil {
		return nil, code.Errorf(code.Bigtable, "failed to get items: %s", err)
	}

	return ret, nil
}
```

`Table.ReadRow()`に`RowList()`を使用し、複数の行を直接取得します。

第3引数で定義されている無名関数が各行ごとに呼ばれます。ここでは主に取得した値をUnmarshalしたりさらに細かいValidationをかけたりできます。この返り値がfalseになるとストリームはシャットダウンされ`ReadRows()`が返ります。

第4引数は`RowFilter()`によってフィルタリングできますが何も入れなければ全てのセルが返ります。


#### 読み込み時に使用するFilterについて

今回上記で使用したフィルタリングに関する関数は以下の通りです。

##### ChainFilters()
- 複数Filterをまとめる
 
##### ColumnFilter()
- カラム修飾子でフィルタリング

##### FamilyFilter()
- カラムファミリーでフィルタリング

##### LatestNFilter()
- フィルターをかけた上で引数の数だけrowを返す

# 最後に

今回はBigtableの概要とそのデータ構造、Golangを使ったデータの読み書きについて説明しました。

少しでも理解のきっかけになれれば幸いです。

Twitterやってるので良かったらフォローお願いします！

Twitter: https://twitter.com/kiokisun_prog