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 を用い た操作を確認した。

脚注

注釈


1

awslは自分用のawsコマンドのラッパーであり、実装は https://github.com/TakesxiSximada/emacs.d/blob/main/bin/awsl にある。APIの向き先をlocalhostに向けられるようにしてあるだけだ。