こんにちは。ライクル事業部エンジニアの寺戸です。
ライクルではサービスの基盤に AWS を採用しており、 AWS の各サービスを利用して機能を提供しています。
バックエンドの開発には Go を採用しており、 AWS 関連の処理は基本的に AWS SDK for Go を使用して開発しています。
AWS SDK for Go はバージョン 2 が2021 年 1 月 19 日に一般公開され、約 1 年が経過した頃合いです。
基本的な書き方は v1 とさほど変わりませんが、細かいところでは変わっている印象です。
ただ、 Developer Guide を読んでもバージョン 2 におけるサンプルコードはまだまだ充実しておらず、 DynamoDB まわりの実装でどう書くんだろう?と悩んだケースがあったので今回はそちらをまとめていきます。
ちなみにバージョン 2 向けの Developers Guide には以下のように記載があるため、基本的な使い方やメソッドの詳細を調べるにはバージョン 1 の Developers Guideを参照した方がよさそうです。
Use the AWS SDK for Go Developer Guide to help you install, configure, and use the SDK. The guide provides configuration information, sample code, and an introduction to the SDK utilities.
※DeepL による翻訳
SDK のインストール、設定、および使用には、AWS SDK for Go Developer Guide を使用してください。このガイドでは、設定情報、サンプルコード、および SDK ユーティリティの紹介を提供しています。
DynamoDB の CRUD について
SDK での実装に入る前に SKD で用意されているメソッドたちがどういう動きをするのかを簡単なコードと共にまとめます。
今回サンプルで作ったコードの全体
v1 の SDK とはパッケージのパスや命名が異なっているので v2 で書く場合の参考まで。
この後 CRUD ごとに解説します。
package main import ( "context" "fmt" "log" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue" "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression" "github.com/aws/aws-sdk-go-v2/service/dynamodb" "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" ) const ( region string = "ap-northeast-1" ) type User struct { Name string `dynamodbav:"name"` // NOTE: Name をパーティションキーとしている Email string `dynamodbav:"email"` Age uint `dynamodbav:"age"` Height uint `dynamodbav:"height"` Weight uint `dynamodbav:"weight"` } func main() { var err error var ctx = context.Background() // DynamoDB クライアントの生成 c, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) if err != nil { fmt.Printf("load aws config: %s\n", err.Error()) return } client := dynamodb.NewFromConfig(c) // 新規レコードの追加 putInput := User{ Name: "Taro Tanaka", Email: "tanaka@example.com", Age: 25, Height: 175, Weight: 65, } av, err := attributevalue.MarshalMap(putInput) if err != nil { fmt.Printf("dynamodb marshal: %s\n", err.Error()) return } _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ TableName: aws.String("user"), Item: av, }) if err != nil { fmt.Printf("put item: %s\n", err.Error()) return } // 既存レコードの取得 getInput := &dynamodb.GetItemInput{ TableName: aws.String("user"), Key: map[string]types.AttributeValue{ "name": &types.AttributeValueMemberS{ Value: "Taro Tanaka", }, }, } output, err := client.GetItem(ctx, getInput) if err != nil { fmt.Printf("get item: %s\n", err.Error()) return } gotUser := User{} err = attributevalue.UnmarshalMap(output.Item, &gotUser) if err != nil { fmt.Printf("dynamodb unmarshal: %s\n", err.Error()) return } fmt.Println(gotUser) // 既存レコードの更新 update := expression.UpdateBuilder{}.Set(expression.Name("height"), expression.Value(180)) // 他のフィールドも更新したいなら以下のように追加可能 update = update.Set(expression.Name("weight"), expression.Value(70)) expr, err := expression.NewBuilder().WithUpdate(update).Build() if err != nil { fmt.Printf("build update expression: %s\n", err.Error()) return } updateInput := &dynamodb.UpdateItemInput{ TableName: aws.String("user"), Key: map[string]types.AttributeValue{ "name": &types.AttributeValueMemberS{ Value: "Taro Tanaka", }, }, ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), UpdateExpression: expr.Update(), } _, err = client.UpdateItem(ctx, updateInput) if err != nil { fmt.Printf("update item: %s\n", err.Error()) return } // レコードの削除 deleteInput := &dynamodb.DeleteItemInput{ TableName: aws.String("user"), Key: map[string]types.AttributeValue{ "name": &types.AttributeValueMemberS{ Value: "Taro Tanaka", }, }, } _, err = client.DeleteItem(ctx, deleteInput) if err != nil { fmt.Printf("delete item: %s\n", err.Error()) return } }
テーブルの用意
以下のような DynamoDB のテーブルを用意しました。 今回はパーティションキーのみを設定しています。
クライアント生成まで
実行時のプロファイルを元に DynamoDB クライアントを生成します。
var ctx = context.Background() // DynamoDB クライアントの生成 c, err := config.LoadDefaultConfig(ctx, config.WithRegion(region)) if err != nil { log.Fatalln("can't load aws config") return } client := dynamodb.NewFromConfig(c)
Create
PutItem
メソッドを使用します。
type User struct { Name string `dynamodbav:"name"` // NOTE: Name をパーティションキーとしている Email string `dynamodbav:"email"` Age uint `dynamodbav:"age"` Height uint `dynamodbav:"height"` Weight uint `dynamodbav:"weight"` } // 新規レコードの追加 putInput := User{ Name: "Taro Tanaka", Email: "tanaka@example.com", Age: 25, Height: 175, Weight: 65, } av, err := attributevalue.MarshalMap(putInput) if err != nil { fmt.Printf("dynamodb marshal: %s\n", err.Error()) return } _, err = client.PutItem(ctx, &dynamodb.PutItemInput{ TableName: aws.String("user"), Item: av, }) if err != nil { fmt.Printf("put item: %s\n", err.Error()) return }
dynamodbav
タグを指定することで DynamoDB のキーと構造体をマッピングすることが出来ます。
dynamodb.PutItemInput
構造体に作成するレコードのデータを埋め込んでいくのですが、愚直に書いていくととても大変なので attributevalue
パッケージを利用して構造体をマップに変換します。
※v1 のドキュメントなのでパッケージ構成が異なっている点に注意
Read
GetItem
メソッドを使用します。
// 既存レコードの取得 getInput := &dynamodb.GetItemInput{ TableName: aws.String("user"), Key: map[string]types.AttributeValue{ "name": &types.AttributeValueMemberS{ Value: "Taro Tanaka", }, }, } output, err := client.GetItem(ctx, getInput) if err != nil { fmt.Printf("get item: %s\n", err.Error()) return } gotUser := User{} err = attributevalue.UnmarshalMap(output.Item, &gotUser) if err != nil { fmt.Printf("dynamodb unmarshal: %s\n", err.Error()) return } fmt.Println(gotUser) // Output: {Taro Tanaka tanaka@example.com 25 175 65}
レコードを取得する際はパーティションキーを指定する必要があります。
今回、 name
というフィールドをパーティションキーとしてテーブルを作成したため、 Key
フィールドの AttributeValue のキー名に name
を指定しています。
ちなみに、今回はテーブル作成時にパーティションキーしか指定しなかったのでコード上でもパーティションキーのみの指定になっていますが、ソートキーも指定した場合はコード上でもソートキーの指定が必要になります。(Key
のマップの要素が 1 つ増えるだけ)
name
は DynamoDB 上では文字列として扱うため AttributeValueMemberS
を指定していますが、数値で扱いたい時は AttributeValueMemberN
にするなど、適切な構造体を選択しましょう。
この辺りがドキュメントなどから見つけられなかったので少し躓いたポイントでした。
Update
UpdateItem
メソッドを使用します。
// 既存レコードの更新 update := expression.UpdateBuilder{}.Set(expression.Name("height"), expression.Value(180)) // 他のフィールドも更新したいなら以下のように追加可能 update = update.Set(expression.Name("weight"), expression.Value(70)) expr, err := expression.NewBuilder().WithUpdate(update).Build() if err != nil { fmt.Printf("build update expression: %s\n", err.Error()) return } updateInput := &dynamodb.UpdateItemInput{ TableName: aws.String("user"), Key: map[string]types.AttributeValue{ "name": &types.AttributeValueMemberS{ Value: "Taro Tanaka", }, }, ExpressionAttributeNames: expr.Names(), ExpressionAttributeValues: expr.Values(), UpdateExpression: expr.Update(), } _, err = client.UpdateItem(ctx, updateInput) if err != nil { fmt.Printf("update item: %s\n", err.Error()) return }
いよいよ複雑になってきました。 ただ、 UpdateItem の場合でも基本的に
- パーティションキーを指定する
- 操作対象のキーと値 (Attribute) を指定する
ということは同じです。
expression
パッケージを使用して Attribute の更新式をビルドすることで更新します。
詳しくは expression パッケージのドキュメントをお読みください…と言いたいところですが、これもまた v1 のドキュメントなので参考にする際はご注意ください。
(参考) https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/expression/
Delete
DeleteItem
メソッドを使用します。
// レコードの削除 deleteInput := &dynamodb.DeleteItemInput{ TableName: aws.String("user"), Key: map[string]types.AttributeValue{ "name": &types.AttributeValueMemberS{ Value: "Taro Tanaka", }, }, } _, err = client.DeleteItem(ctx, deleteInput) if err != nil { fmt.Printf("delete item: %s\n", err.Error()) return }
Delete の場合はシンプルにパーティションキーを指定するだけですね。
PutItem と UpdateItem の違い
DynamoDB を操作する際に重要だと感じたところは PutItem
と UpdateItem
の挙動の違いです。
以下にまとめます。
PutItem
https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/#DynamoDB.PutItem
Creates a new item, or replaces an old item with a new item. If an item that has the same primary key as the new item already exists in the specified table, the new item completely replaces the existing item. You can perform a conditional put operation (add a new item if one with the specified primary key doesn't exist), or replace an existing item if it has certain attribute values. You can return the item's attribute values in the same operation, using the ReturnValues parameter.
※DeepL による翻訳
新しいアイテムを作成するか、古いアイテムを新しいアイテムで置き換えます。指定したテーブルに新しいアイテムと同じ主キーを持つアイテムが既に存在する場合、新しいアイテムは既存のアイテムを完全に置き換えます。条件付き put 操作 (指定した主キーを持つ項目が存在しない場合に新しい項目を追加する) や、既存の項目が特定の属性値を持っている場合にその項目を置き換えることもできます。ReturnValues パラメータを使えば、同じ操作で項目の属性値を返すことができます。
指定したキーのレコードが存在していなければ新しいレコードとして登録するし、存在していればその情報を与えられた情報で全て上書きする、なるほど。
つまり、 PutItem
実行時に指定されていないキーについては削除されてしまう、ということですね。
メソッド名の通り、HTTP でいう PUT と同じ動きと考えればよさそうです。
UpdateItem
https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/#DynamoDB.UpdateItem
Edits an existing item's attributes, or adds a new item to the table if it does not already exist. You can put, delete, or add attribute values. You can also perform a conditional update on an existing item (insert a new attribute name-value pair if it doesn't exist, or replace an existing name-value pair if it has certain expected attribute values).
※DeepL による翻訳
既存のアイテムの属性を編集し、まだ存在しない場合は新しいアイテムをテーブルに追加します。属性の値を入れたり、削除したり、追加したりすることができます。また、既存のアイテムに対して条件付き更新を行うこともできます(存在しない場合は新しい属性の名前と値のペアを挿入し、特定の期待される属性値を持っている場合は既存の名前と値のペアを置き換えます)。
対してこちらは指定したキーのレコードの、指定したフィールドだけに対して操作が行われるということですね。
HTTP でいう PATCH と同じ動きと考えればよさそうです。
終わりに
今回は AWS SDK for Go v2 における DynamoDB の基本的な操作についてまとめました。
公式ドキュメントを見てもなかなか正解の書き方に辿り着けずに苦労したので、同じように悩んでいる方の参考になれば幸いです。
SO Technologies では一緒に開発を行っていただけるエンジニアを大募集しております!
Go や AWS を使用した開発に少しでも興味があれば是非お気軽にお声がけいただければと思います!