😸

【Go】LocalStack+DynamoDBでCRUD操作

2025/02/16に公開

はじめに

現在携わっているプロジェクトで、go-redisを使ったRedis操作を行いました。
同じNoSQLであるDynamoDBならどのように操作できるのか興味を持ち、調べ実装したので、メモ的に書いていこうと思います!

実装

LocalStack環境構築

今回はAWSの認証情報に渡すキーをtestと設定

docker-compose.yaml
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の本番環境は使わないので、リージョンは仮で設定

main.go
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関数で呼び出している)

main.go
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関数で呼び出す)

main.go
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, &notFoundError); 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関数で呼び出している)

main.go
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関数で呼び出している)

main.go
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関数で呼び出している)

main.go
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関数で呼び出している)

main.go
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関数で呼び出している)

main.go
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