GoでDynamoDBを操作する。ライブラリとしてはaws-sdk-goを使用する。 dynamoなどの 便利なラッパー が開発されているが、 そのようなラッパーを使用していると実際に行っている操作が見えにくくなる。 そのため今回は使用しない。
テーブル定義
以下のテーブル定義を用いる。プライマリーインデックスにcodeという属性、 セカンダリインデックスにexternalCodeという属性を指定している。
table-definition.json::
{
"AttributeDefinitions": [
{
"AttributeName": "Id",
"AttributeType": "S"
},
{
"AttributeName": "Name",
"AttributeType": "S"
}
],
"KeySchema": [
{
"AttributeName": "Id",
"KeyType": "HASH"
}
],
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
},
"GlobalSecondaryIndexes": [
{
"IndexName": "Name",
"KeySchema": [
{
"AttributeName": "Name",
"KeyType": "HASH"
}
],
"Projection": {
"ProjectionType": "ALL"
},
"ProvisionedThroughput": {
"ReadCapacityUnits": 1,
"WriteCapacityUnits": 1
}
}
]
}
awsコマンドでテーブルを作成する。
awsl dynamodb create-table --table-name 'example' --cli-input-json file://table-definition.json
ここではawslコマンドを使用しているが、lを除去してawsコマンドに置き換えて欲しい1。
クライアントの取得
DynamoDBのクライアントの取得する。 接続先は今回はlocalで動作させているlocalstackとする。 接続情報の受け渡しについては今回はスコープ外のため固定値としてコードに直接記述した。
レコードの保存
PutItemを用いてレコードを保存する。
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
func main() {
svc := GetDynamoDBService()
input := &dynamodb.PutItemInput{
Item: map[string]*dynamodb.AttributeValue{
"Id": {
S: aws.String("foo"),
},
"Name": {
S: aws.String("bar"),
},
},
ReturnConsumedCapacity: aws.String("TOTAL"),
TableName: aws.String("example"),
}
result, err := svc.PutItem(input)
if err != nil {
fmt.Println("ERROR")
return
}
fmt.Println(result)
}
go run
で実行する。
go run put.go lib.go
{ ConsumedCapacity: { CapacityUnits: 1, TableName: "example" } }
レコードの取得
GetItemを用いてレコードを取得する。
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
func main() {
svc := GetDynamoDBService()
input := &dynamodb.GetItemInput{
Key: map[string]*dynamodb.AttributeValue{
"Id": {
S: aws.String("foo"),
},
},
TableName: aws.String("example"),
}
result, err := svc.GetItem(input)
if err != nil {
fmt.Println(err)
return
}
if result.Item == nil {
fmt.Println("No item")
return
}
fmt.Println(result)
}
先程と同様に go run
で実行する。
go run get.go lib.go
{ Item: { Id: { S: "foo" }, Name: { S: "bar" } } }
レコードの型定義
ここまでの処理を押えておけばDynamoDB自体は操作できる。 ただしプログラムからレコードのデータを直接触るのではなく、 専用の型を宣言したほうが扱いやすく 型チェックの恩恵なども得られるため不具合も減らせる。
今回はExample型を1つだけ定義する。
定義した型のインスタンスをレコードとして保存する
github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute
を使用すると
独自に定義した型の値を変換することでレコードとして保存できる。
上述の定義した型を使用してレコードを保存する。
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
)
func main() {
example := Example{
Id: "foo",
Name: "bar",
}
marshalized_example, err := dynamodbattribute.MarshalMap(example)
if err != nil {
fmt.Println(err)
return
}
svc := GetDynamoDBService()
input := &dynamodb.PutItemInput{
Item: marshalized_example,
TableName: aws.String("example"),
}
result, err := svc.PutItem(input)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(result)
}
実行する。
go run put_with_marshal.go lib.go types.go
{ ConsumedCapacity: { CapacityUnits: 1, TableName: "example" } }
前述したレコードの保存と同様に処理が成功することを確認できる。
定義した型のインスタンスに変換する
次は取得したレコードを独自に定義した型にマッピングする。
dynamodbattribute.UnmarshalMap()
を用いてExample型の値に変換する。
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
"example/lib"
"reflect"
)
type Example struct {
Id string `json:"id,omitempty"`
Data map[string]*dynamodb.AttributeValue `json:"data,omitempty"`
}
func main() {
svc := lib.GetDynamoDBService()
input := &dynamodb.GetItemInput{
Key: map[string]*dynamodb.AttributeValue{
"id": {
S: aws.String("foo"),
},
},
TableName: aws.String("example"),
}
result, err := svc.GetItem(input)
if err != nil {
fmt.Println(err)
return
}
if result.Item == nil {
fmt.Println("No item")
return
}
d := new(Example)
err = dynamodbattribute.UnmarshalMap(result.Item, &d)
if err != nil {
fmt.Println("Failed to decode object")
return
}
n, ok := d.Data["a"]
fmt.Println(ok)
fmt.Println(n)
}
実行する。
go run get_with_unmarshal.go
foo bar
Example型の値にアクセスする形で出力されていることが分かる。
条件付きPutItem
DynamoDBには条件付きで処理を行うための機構がある。 例えばキーが重複していなければ保存するといった処理を行える。
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
func main() {
svc := GetDynamoDBService()
input := &dynamodb.PutItemInput{
Item: map[string]*dynamodb.AttributeValue{
"Id": {
S: aws.String("aaaa"),
},
"Name": {
S: aws.String("bar"),
},
},
ReturnConsumedCapacity: aws.String("TOTAL"),
TableName: aws.String("example"),
ConditionExpression: aws.String("attribute_not_exists(Id)"),
}
result, err := svc.PutItem(input)
if err != nil {
panic(err.Error())
}
fmt.Println(result)
}
前述のPutItemとの違いはConditionExpressionを指定しているところだ。 ConditionExpressionに各種条件を設定する。今回はIdが重複しなければという条件になっている。
attribute_not_exists(Id)
条件に合致しない場合PutItemは実施されず、エラーが返されることに注意が 必要となる。
レコードの削除
指定したレコードを削除する。Idが foo
であるレコードが存在するこ
とを前提に、そのレコードを削除する。
package main
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/dynamodb"
)
func main() {
svc := GetDynamoDBService()
input := &dynamodb.DeleteItemInput{
Key: map[string]*dynamodb.AttributeValue{
"Id": {
S: aws.String("foo"),
},
},
ReturnConsumedCapacity: aws.String("TOTAL"),
TableName: aws.String("example"),
}
result, err := svc.DeleteItem(input)
if err != nil {
fmt.Println("ERROR")
return
}
fmt.Println(result)
}
実行する。
go run delete_item.go lib.go
{ ConsumedCapacity: { CapacityUnits: 2, TableName: "example" } }
指定したKeyに一致するレコードが削除される。
辞書として保存した属性を読み出す
go run unmarshal_map.go
まとめ
aws-sdk-goを使用してDynamoDBの基本的な操作であるGetItem、PutItem、
DeleteItemを確認した。またレコードを便利に扱うために独自の型定義にマッ
ピングするための
github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute
を用い
た操作を確認した。
脚注
注釈
awslは自分用のawsコマンドのラッパーであり、実装は https://github.com/TakesxiSximada/emacs.d/blob/main/bin/awsl にある。APIの向き先をlocalhostに向けられるようにしてあるだけだ。