【Go】LocalStack+DynamoDBでCRUD操作
はじめに
現在携わっているプロジェクトで、go-redisを使ったRedis操作を行いました。
同じNoSQLであるDynamoDBならどのように操作できるのか興味を持ち、調べ実装したので、メモ的に書いていこうと思います!
実装
LocalStack環境構築
今回はAWSの認証情報に渡すキーをtestと設定
version: '3.8'
services:
localstack:
image: localstack/localstack
ports:
- "4566:4566"
environment:
- SERVICES=dynamodb
- DEFAULT_REGION=us-east-1
- EDGE_PORT=4566
- AWS_ACCESS_KEY_ID=test
- AWS_SECRET_ACCESS_KEY=test
DynamoDBクライアント設定まで
今回はAWSの本番環境は使わないので、リージョンは仮で設定
func main() {
ctx := context.TODO()
if err := godotenv.Load(); err != nil {
log.Fatalf("環境変数読み込みエラー:%v", err)
}
cfg, err := config.LoadDefaultConfig(ctx,
config.WithRegion("us-east-1"),
config.WithCredentialsProvider(aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider(os.Getenv("AWS_ACCESS_KEY_ID"), os.Getenv("AWS_SECRET_ACCESS_KEY"), ""))),
)
if err != nil {
log.Fatalf("AWS SDK設定読み込みエラー: %v", err)
}
client := dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) {
o.BaseEndpoint = aws.String("http://localhost:4566")
})
}
テーブル作成
テーブル作成をするためには、CreateTableメソッドを使用
Usersテーブルを作成し、UserIDを主キーとし、Hash型として定義
(今回はcreateTable関数を作成、main関数で呼び出している)
func createTable(ctx context.Context, client *dynamodb.Client) error {
_, err := client.CreateTable(ctx, &dynamodb.CreateTableInput{
TableName: aws.String("Users"),
AttributeDefinitions: []types.AttributeDefinition{
{
AttributeName: aws.String("UserID"),
AttributeType: types.ScalarAttributeTypeS,
},
},
KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String("UserID"),
KeyType: types.KeyTypeHash,
},
},
BillingMode: types.BillingModePayPerRequest,
})
if err != nil {
return err
}
return nil
}
// main関数で呼び出し
err = createTable(ctx, client)
if err != nil {
log.Fatalf("テーブル作成エラー:%v", err)
}
fmt.Println("テーブル作成完了")
※注意点※
同名のテーブルを作成しようとすると重複エラーになるので、
既に存在しているか否かをチェックする必要あり
テーブルの状態をチェックするためにはDescribeTableメソッドを使用
(今回はtableExists関数を作成、main関数で呼び出す)
func tableExists(ctx context.Context, client *dynamodb.Client) (bool, error) {
_, err := client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String("Users"),
})
if err != nil {
var notFoundError *types.ResourceNotFoundException
if ok := errors.As(err, ¬FoundError); ok {
return false, nil
}
return false, err
}
return true, nil
}
// main関数で呼び出し
exists, err := tableExists(ctx, client)
if err != nil {
log.Fatalf("テーブル存在チェックエラー:%v", err)
}
if exists {
log.Fatalf("テーブルは既に存在しています:%v", err)
}
データ追加
データを追加をするためには、PutItemメソッドを使用
(今回はputItem関数を作成、main関数で呼び出している)
func putItem(ctx context.Context, client *dynamodb.Client) error {
_, err := client.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String("Users"),
Item: map[string]types.AttributeValue{
"UserID": &types.AttributeValueMemberS{Value: "1"},
"Name": &types.AttributeValueMemberS{Value: "Taro"},
"Age": &types.AttributeValueMemberN{Value: "20"},
},
})
return err
}
// main関数で呼び出し
err = putItem(ctx, client)
if err != nil {
log.Fatalf("データ追加エラー:%v", err)
}
fmt.Println("データ作成完了")
データ詳細取得
詳細データを取得をするためには、GetItemメソッドを使用
ターミナル出力用に、JSON形式に変換
(今回はgetItem関数を作成、main関数で呼び出している)
func getItem(ctx context.Context, client *dynamodb.Client) (string, error) {
item, err := client.GetItem(ctx, &dynamodb.GetItemInput{
TableName: aws.String("Users"),
Key: map[string]types.AttributeValue{
"UserID": &types.AttributeValueMemberS{Value: "1"},
},
})
if err != nil {
return "", err
}
jsonData, err := json.Marshal(item)
if err != nil {
return "", err
}
return string(jsonData), nil
}
// main関数で呼び出し
item, err := getItem(ctx, client)
if err != nil {
log.Fatalf("テータ取得エラー:%v", err)
}
fmt.Println(item)
出力結果
{"ConsumedCapacity":null,"Item":{"Age":{"Value":"20"},"Name":{"Value":"Taro"},"UserID":{"Value":"1"}},"ResultMetadata":{}}
データ一覧取得
一覧データを取得をするためには、Scanメソッドを使用
ターミナル出力用に、マップのキーに紐づく値の型を変換してからJSON形式に変換
(今回はscanItems関数を作成、main関数で呼び出している)
func scanItems(ctx context.Context, client *dynamodb.Client) (string, error) {
items, err := client.Scan(ctx, &dynamodb.ScanInput{
TableName: aws.String("Users"),
})
if err != nil {
return "", err
}
var result []map[string]any
for _, item := range items.Items {
mappedItem := make(map[string]any)
for key, value := range item {
mappedItem[key] = convertAttributeValue(value)
}
result = append(result, mappedItem)
}
jsonData, err := json.MarshalIndent(result, "", " ")
if err != nil {
return "", err
}
return string(jsonData), nil
}
// キーに紐づく値ごとにデータ変換
func convertAttributeValue(av types.AttributeValue) any {
switch v := av.(type) {
case *types.AttributeValueMemberS:
return v.Value
case *types.AttributeValueMemberN:
return v.Value
case *types.AttributeValueMemberBOOL:
return v.Value
case *types.AttributeValueMemberM:
mapped := make(map[string]any)
for key, val := range v.Value {
mapped[key] = convertAttributeValue(val)
}
return mapped
case *types.AttributeValueMemberL:
var list []any
for _, val := range v.Value {
list = append(list, convertAttributeValue(val))
}
return list
default:
return nil
}
}
// main関数で呼び出し
items, err := scanItems(ctx, client)
if err != nil {
log.Fatalf("全データ取得エラー:%v", err)
}
fmt.Println(items)
出力結果
今回は別途データを追加し、複数取得できるよう設定済
出力ごとに順序が異なることに注意
[
{
"Age": "20",
"Name": "Taro",
"UserID": "1"
},
{
"Age": "22",
"Name": "Hanako",
"UserID": "3"
},
{
"Age": "21",
"Name": "Jiro",
"UserID": "2"
}
]
データ更新
一覧データを取得をするためには、UpdateItemメソッドを使用
Taro
というデータをJiro
に更新
ターミナル出力用に、JSON形式に変換
(今回はupdateItem関数を作成、main関数で呼び出している)
func updateItem(ctx context.Context, client *dynamodb.Client) (string, error) {
item, err := client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
TableName: aws.String("Users"),
Key: map[string]types.AttributeValue{
"UserID": &types.AttributeValueMemberS{Value: "1"},
},
ExpressionAttributeNames: map[string]string{
"#N": "Name",
},
ExpressionAttributeValues: map[string]types.AttributeValue{
":Name": &types.AttributeValueMemberS{Value: "Jiro"},
},
UpdateExpression: aws.String("SET #N = :Name"),
ReturnValues: types.ReturnValueUpdatedNew,
})
if err != nil {
return "", err
}
jsonData, err := json.Marshal(item)
if err != nil {
return "", err
}
return string(jsonData), nil
}
// main関数で呼び出し
updatedItem, err := updateItem(ctx, client)
if err != nil {
log.Fatalf("データ更新エラー:%v", err)
}
fmt.Println(updatedItem)
出力結果
更新前("Value":"Taro"
)
{"ConsumedCapacity":null,"Item":{"Age":{"Value":"20"},"Name":{"Value":"Taro"},"UserID":{"Value":"1"}},"ResultMetadata":{}}
更新後("Value":"Jiro"
)
{"ConsumedCapacity":null,"Item":{"Age":{"Value":"20"},"Name":{"Value":"Jiro"},"UserID":{"Value":"1"}},"ResultMetadata":{}}
データ削除
データを削除するためには、DeleteItemメソッドを使用
(今回はdeleteItem関数を作成、main関数で呼び出している)
func deleteItem(ctx context.Context, client *dynamodb.Client) error {
_, err := client.DeleteItem(ctx, &dynamodb.DeleteItemInput{
TableName: aws.String("Users"),
Key: map[string]types.AttributeValue{
"UserID": &types.AttributeValueMemberS{Value: "1"},
},
})
if err != nil {
return err
}
return nil
}
// main関数で呼び出し
err = deleteItem(ctx, client)
if err != nil {
log.Fatalf("テーブル削除エラー:%v", err)
}
まとめ
SDK V2を使ったDynamoDB操作を見ていきました。
他にもさまざまな機能があるので、深く見てみようと思います!
Discussion