🗂️

【DynamoDB】セカンダリインデックスをGoで扱う

2021/10/11に公開

はじめに

DynamoDBのセカンダリインデックスってよく分からないって人向けに、
実際にGoで扱ってみる記事を書いてみました。
これであなたもセカンダリインデックスが理解出来るはず!?

セカンダリインデックスとは

DynamoDBには2つのセカンダリインデックスがあります。

  • Global Secondary Indexes(グローバルセカンダリインデックス)
  • Local Secondary Indexes(ローカルセカンダリインデックス)

詳しくは書きませんが、DynamoDBはRDSと比べると、検索がかなりしんどいです。
ハッシュキーと呼ばれるキー + レンジキーと呼ばれるキー
に一致するものを取得というものだけしか取得出来ません。

※例 UserIDが1111で、Nameが太郎のレコードと一つ取得

err = userTable.Get("UserID", "1111").Range("Name", dynamo.Equal, "太郎").One(&user)

RDBみたいに、複雑な検索が出来ずに困る事が多いです。
それを少し補うのがセカンダリインデックスです。
セカンダリインデックスは、テーブルのハッシュキーとレンジキー以外の検索の方法で、
別のキーを設置して、その新しいキーで検索するというものです。

Global Secondary Indexes(グローバルセカンダリインデックス)

Global Secondary Indexesはそのテーブルのキーと全く違うキーで検索するインデックスを作成します。

※ 例

//構造体定義
type User struct {
	UserID string `dynamo:"UserID,hash"`
	Name   string `dynamo:"Name,range"`
	Age    int    `dynamo:"Age" index:"index-1,hash"`
	TextID string `dynamo:"TextID" index:"index-1,range"`
}
//テーブル作成
err = db.CreateTable("UserTable", User{}).Run()

この例では、UserTableを作成しています。
このUserTableはUserIDがハッシュキー、Nameがレンジキーです。
なので、UserIDとNameが一致するものを取得出来ます。
しかし、別の検索方法で取得したい場合があります。
そこで、Global Secondary Indexesの出番です。
index-1という名前のindexを作成しました。
index-1はAgeがハッシュキーで、TextIDがレンジキーになっているので、Ageが20で、TextIDが3のものを取得という検索が出来ます。

詳しくは公式をご覧ください。

https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html

Local Secondary Indexes(ローカルセカンダリインデックス)

Local Secondary Indexes(ローカルセカンダリインデックス)は
ハッシュキーはテーブルと同じで、レンジキーだけ違うものです。

※ 例

//構造体定義
type User struct {
	UserID string `dynamo:"UserID,hash"`
	Name   string `dynamo:"Name,range"`
	Age    int    `dynamo:"Age" localIndex:"index-2,range"`
	TextID string `dynamo:"TextID"`
}
//テーブル作成
err = db.CreateTable("UserTable", User{}).Run()

上記のように設定すると、UserIDが1234で、Ageが20の人というように検索出来るようになります。
Local Secondary Indexesはハッシュキーはテーブルと同じでUserIDになります。
Local Secondary Indexesを使う時は構造体のタグでlocalIndexを使用します。
構造体のタグって凄いですね。
こちらも詳しくは公式をご覧ください。

https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html

前置きが長くなってしまったので、早速やっていきます。

環境

  • go 1.15
  • github.com/aws/aws-sdk-go v1.38.0
  • github.com/guregu/dynamo v1.11.0

ソースコード

package main

import (
	"fmt"

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

type User struct {
	UserID string `dynamo:"UserID,hash" index:"index-1,range"`
	Name   string `dynamo:"Name,range"`
	Age    int    `dynamo:"Age" localIndex:"index-2,range" index:"index-1,hash"`
	TextID string `dynamo:"TextID" index:"index-3,hash"`
}

// 本来はenvから取得した方が良い
const AWS_REGION = "ap-northeast-1"
const DYNAMO_ENDPOINT = "http://localhost:8000"

func main() {
	// クライアントの設定
	sess, err := session.NewSession(&aws.Config{
		Region:      aws.String(AWS_REGION),
		Endpoint:    aws.String(DYNAMO_ENDPOINT),
		Credentials: credentials.NewStaticCredentials("dummy", "dummy", "dummy"),
	})
	if err != nil {
		panic(err)
	}

	db := dynamo.New(sess)

	// テーブル作成をする為に、一度テーブルを削除します
	db.Table("UserTable").DeleteTable().Run()

	// テーブル作成
	err = db.CreateTable("UserTable", User{}).Run()
	if err != nil {
		panic(err)
	}

	// テーブルの指定
	userTable := db.Table("UserTable")

	// User構造体をuser変数に定義
	var user User

	err = userTable.Put(&User{UserID: "1111", Name: "太郎", Age: 22, TextID: "1111abc"}).Run()
	if err != nil {
		panic(err)
	}
	err = userTable.Put(&User{UserID: "2222", Name: "花子", Age: 33, TextID: "2222abc"}).Run()
	if err != nil {
		panic(err)
	}
	err = userTable.Put(&User{UserID: "3333", Name: "かずや", Age: 30, TextID: "4444abc"}).Run()
	if err != nil {
		panic(err)
	}
	err = userTable.Put(&User{UserID: "4444", Name: "そら", Age: 20, TextID: "3333abc"}).Run()
	if err != nil {
		panic(err)
	}

	// 通常の取得
	err = userTable.Get("UserID", "1111").Range("Name", dynamo.Equal, "太郎").One(&user)
	if err != nil {
		panic(err)
	}
	fmt.Printf("GetDB%+v\n", user)

	// グローバルセカンダリーインデックスを利用して取得
	err = userTable.Get("Age", 33).Range("UserID", dynamo.Equal, "2222").Index("index-1").One(&user)
	if err != nil {
		panic(err)
	}
	fmt.Printf("GetDB%+v\n", user)

	err = userTable.Get("TextID", "4444abc").Index("index-3").One(&user)
	if err != nil {
		panic(err)
	}
	fmt.Printf("GetDB%+v\n", user)

	// ローカルセカンダリーインデックスを利用して取得
	err = userTable.Get("UserID", "4444").Range("Age", dynamo.Equal, 20).Index("index-2").One(&user)
	if err != nil {
		panic(err)
	}
	fmt.Printf("GetDB%+v\n", user)
}

出力結果

GetDB{UserID:1111 Name:太郎 Age:22 TextID:1111abc}
GetDB{UserID:2222 Name:花子 Age:33 TextID:2222abc}
GetDB{UserID:3333 Name:かずや Age:30 TextID:4444abc}
GetDB{UserID:4444 Name:そら Age:20 TextID:3333abc}

解説

少しだけ

この構造体でテーブルの定義をしています。

type User struct {
	UserID string `dynamo:"UserID,hash" index:"index-1,range"`
	Name   string `dynamo:"Name,range"`
	Age    int    `dynamo:"Age" localIndex:"index-2,range" index:"index-1,hash"`
	TextID string `dynamo:"TextID" index:"index-3,hash"`
}

このテーブルはUserIDがハッシュキー、Nameがレンジキーです。
index-1がGlobal Secondary IndexesでハッシュキーがAge、レンジキーがUserID
index-2はLocal Secondary IndexesでハッシュキーがUserID、レンジキーがAge(index-1の逆ですね)
index-3はGlobal Secondary IndexesでハッシュキーがTextIDでレンジキーは無し

です。
このように一つのフィールドにGlobal Secondary IndexesとLocal Secondary Indexesどちらもタグで設定する事が可能です。

さいごに

これであなたもセカンダリインデックスマスターですね。

参考

Discussion