3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

この記事は CyberAgent 22 新卒 Advent Clendar 2021 16日目の記事です。

概要

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

そもそもBigtableとは

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

公式Document

データ構造

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

  • RowKey(行キー),

  • ColumnQualifier(列修飾子)

  • ColumnFamily(列ファミリー)

  • Timestamp(タイムスタンプ)

  • Value(バリュー)

で構成されています。

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

以下設計です。

お気に入りリストテーブル(favorite)
行キー 列ファミリー:列修飾子 タイムスタンプ
UserID value:<ContentID> 0 <FavoriteInfo>
UserID addedAt:<AddedAt> 0 <FavoriteInfo>
  • <ContentID>はお気に入りコンテンツのIDです。Stringです。
  • <AddedAt>はお気に入り情報の追加日時です
  • <FavoriteInfo>はお気に入り情報です。構造体です。
  • 今回タイムスタンプは簡略化のため0で設定しています。

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

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

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

スクリーンショット 2021-12-16 11.21.03.png

GoによるBigtableの読み書きについて

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

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

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

書き込み

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

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というデータを保存する架空の関数を例にします

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を返す関数を入れて使います。

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
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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?