aminev.blog

Go言語でDynamoDBのセット型を扱う

要約

  • Go 言語で DynamoDB のセット型を扱うことはできる
  • ただし、要素の追加や削除が面倒
  • リスト型で十分な場合はリスト型を使うべき

前提

各ライブラリのバージョンは以下のとおりです。

github.com/aws/aws-sdk-go-v2 v1.16.16
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.10.0
github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.4.26
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.17.1

また、サンプルコードは以下に置いています。

https://github.com/Aminevsky/go-dynamodb-set-sample

セット型とは

AWS ドキュメント によると、 DynamoDB のセット型には以下のような特徴があります。

  • 数値セット、文字列セット、バイナリセットの 3 種類のみ
  • 要素は全て同じ型でなければならない
  • 要素は一意でなければならない(重複不可)
  • 要素の順序は保持されない

たとえば、リスト型は重複を認めるので、次のように格納することができます。

[ 1, 1, 2, 3, 4, 5]

一方、セット型では、必ず、次のように格納されます。

[ 1, 2, 3, 4, 5]

テーブル例

ここでは、次のようなテーブルを想定します。

属性名キーデータ型
idパーティションキー文字列
team_name文字列
batting_orderリスト
reserve数値セット

型定義

feature/dynamodb/attributevalueMarshal() は、デフォルトでスライスをリスト型へ変換します。

https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue#Marshal

Marshaling slices to AttributeValue will default to a List for all types except for []byte and [][]byte.

Marshal() でリスト型ではなくセット型へ変換するためには、 構造体タグで明示する必要があります。以下では、フィールド Reservenumberset を指定することで、数値セットであることを明示しています。

type BaseballTeam struct {
    ID           string `dynamodbav:"id"`
    TeamName     string `dynamodbav:"team_name"`
    BattingOrder []int  `dynamodbav:"batting_order"`
    Reserve      []int  `dynamodbav:"reserve,numberset"`
}

要素追加

DynamoDB では、セット型に要素を追加するときは、更新式 UPDATEADD アクションを使います。

Go 言語で書いた場合は以下のようになります。

func (r *BaseballTeamRepository) AddReserve(ctx context.Context, id string, addNumbers []int) error {
	key, err := attributevalue.MarshalMap(map[string]string{"id": id})
	if err != nil {
		return fmt.Errorf("marshal error: %w", err)
	}

	// each element must be string
	params := make([]string, 0, len(addNumbers))
	for _, n := range addNumbers {
		params = append(params, strconv.Itoa(n))
	}

	// value is number set, not number list
	update := expression.Add(expression.Name("reserve"), expression.Value(types.AttributeValueMemberNS{Value: params}))
	builder, err := expression.NewBuilder().WithUpdate(update).Build()
	if err != nil {
		return fmt.Errorf("builder error: %w", err)
	}

	_, err = r.client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
		Key:                       key,
		TableName:                 aws.String(r.tableName),
		ExpressionAttributeNames:  builder.Names(),
		ExpressionAttributeValues: builder.Values(),
		UpdateExpression:          builder.Update(),
	})
	if err != nil {
		return fmt.Errorf("add to reserve failed: %w", err)
	}

	return nil
}

expression.Value() の引数としてスライスを直接与えることはできません。デフォルトでスライスはリスト型へ変換されるからです。 types.AttributeValueMemberNS を使って明示する必要があります。

update := expression.Add(expression.Name("reserve"), expression.Value(types.AttributeValueMemberNS{Value: params}))

また、 types.AttributeValueMemberNS のフィールド Value の型は []string なので、それに合わせる必要があります。そのため、以下のように []int[]string へ変換しています。

params := make([]string, 0, len(addNumbers))
for _, n := range addNumbers {
    params = append(params, strconv.Itoa(n))
}

要素削除

セット型から要素を削除するときは、更新式 UPDATEDELETE を使います。

Go 言語で書いた場合は以下のようになります。ADD のときと同様に、あらかじめ []string へ変換しておき、 types.AttributeValueMemberNS を使う必要があります。

func (r *BaseballTeamRepository) DeleteReserve(ctx context.Context, id string, deleteNumbers []int) error {
	key, err := attributevalue.MarshalMap(map[string]string{"id": id})
	if err != nil {
		return fmt.Errorf("marshal error: %w", err)
	}

	// each element must be string
	params := make([]string, 0, len(deleteNumbers))
	for _, n := range deleteNumbers {
		params = append(params, strconv.Itoa(n))
	}

	update := expression.Delete(expression.Name("reserve"), expression.Value(types.AttributeValueMemberNS{Value: params}))
	builder, err := expression.NewBuilder().WithUpdate(update).Build()
	if err != nil {
		return fmt.Errorf("builder error: %w", err)
	}

	_, err = r.client.UpdateItem(ctx, &dynamodb.UpdateItemInput{
		Key:                       key,
		TableName:                 aws.String(r.tableName),
		ExpressionAttributeNames:  builder.Names(),
		ExpressionAttributeValues: builder.Values(),
		UpdateExpression:          builder.Update(),
	})
	if err != nil {
		return fmt.Errorf("delete from reserve failed: %s", err)
	}

	return nil
}

まとめ

AWS SDK for Go v2 のデフォルトでは、スライスはリスト型として扱われます。 そのため、セット型を使うためには、構造体タグで明示的に指定したり、 []string へ変換する必要があります。

セット型をどうしても使わなければならない場合以外は、リスト型を使うべきでしょう。