CDK×Lambda×golang×Dynamoでアプリを作ってみる DynamoDB処理のLambda実装編GET&POST（第四回）

さて、今回は以下のようにファイルを記述してAPIGatewayで用意したエンドポイントへアクセスし、DynamoDBのレコードを作成＆取得できるようにしていきます。長くなってしまうので今回はGETとPOSTをまずは作成し、次回更新と削除を実装していきたいと思います。

いつも通り詳しい解説をしていきます。
```lambda/dynamoDBHandler/main.go
package main

import (
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
	"github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
	"encoding/json"
	"net/http"
)

var (
	tableName = "MyDynamoDB"
	dynamoDb  = dynamodb.New(session.Must(session.NewSession()))
)

type Item struct {
	ID      string `json:"id"`
	Content string `json:"content"`
}

func getItem(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	id := request.QueryStringParameters["id"]

	result, err := dynamoDb.GetItem(&dynamodb.GetItemInput{
		TableName: aws.String(tableName),
		Key: map[string]*dynamodb.AttributeValue{
			"id": {
				S: aws.String(id),
			},
		},
	})

	if err != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusInternalServerError,
			Body:       err.Error(),
		}, nil
	}

	item := Item{}
	err = dynamodbattribute.UnmarshalMap(result.Item, &item)
	if err != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusInternalServerError,
			Body:       err.Error(),
		}, nil
	}

	body, _ := json.Marshal(item)
	return events.APIGatewayProxyResponse{
		StatusCode: http.StatusOK,
		Body:       string(body),
	}, nil
}

func putItem(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	item := Item{}
	err := json.Unmarshal([]byte(request.Body), &item)
	if err != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusBadRequest,
			Body:       err.Error(),
		}, nil
	}

	_, err = dynamoDb.PutItem(&dynamodb.PutItemInput{
		TableName: aws.String(tableName),
		Item: map[string]*dynamodb.AttributeValue{
			"id": {
				S: aws.String(item.ID),
			},
			"content": { // Content 属性を追加
				S: aws.String(item.Content),
			},
		},
	})

	if err != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusInternalServerError,
			Body:       err.Error(),
		}, nil
	}

	return events.APIGatewayProxyResponse{
		StatusCode: http.StatusCreated,
	}, nil
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	switch request.HTTPMethod {
	case "GET":
		return getItem(request)
	case "POST":
		return putItem(request)
	default:
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusMethodNotAllowed,
		}, nil
	}
}

func main() {
	lambda.Start(handler)
}
```

まずは、以下のようにItem構造体を定義します。
ここでは受け取ったJSON形式のバイトスライスを格納するために使います。
今回はIDとContentという簡単な構成にしておきます。

そして、構造体のキーがそのまま使用されてしまうので、それを回避するために定義でjsonを使用する際のキー名をjson:"hoge"のように指定します。

```
type Item struct {
	ID      string `json:"id"`
	Content string `json:"content"`
}
```

## getItem
続いて`getItem`関数を見ていきます。
引数には`events.APIGatewayProxyRequest`を取ります。
この構造体は以下の通りです。

今回はクエリパラメータからidを取得するようにしたいので`QueryStringParameters`を使用します。
```
type APIGatewayProxyRequest 構造体 {
 	Resource                         string                         `json:"resource"` // API Gateway で定義されたリソース パス
	Path                             string                         `json:"path"`      // 呼び出し元の URL パス
	HTTPMethod                       string                         `json:"httpMethod"`
 	Headers map[ string ] string              `json:"headers"`
 	MultiValueHeaders map[ string ][] string            `json:"multiValueHeaders"`
 	QueryStringParameters map[ string ] string              `json:"queryStringParameters"`
 	MultiValueQueryStringParameters map[ string ][] string            `json:"multiValueQueryStringParameters"`
 	PathParameters map[ string ] string              `json:"pathParameters"`
 	StageVariables map[ string ] string              `json:"stageVariables"`
 	RequestContext                   APIGatewayProxyRequestContext `json:"requestContext"`
 	Body                             string                         `json:"body"`
 	IsBase64Encoded                  bool                           `json:"isBase64Encoded,omitempty"`
 }
```

以下のように書くことで引数のeventsからクエリパラメータのidを取得できるので、こちらを変数idに格納します。
```
id := request.QueryStringParameters["id"]
```

次に`dynamoDb.GetItem`で取得します。GetItem操作は、指定された主キーを持つアイテムの属性セットを返します。一致するアイテムがない場合、GetItem はデータを返さず、応答にItem要素は含まれません。

`input`で定義している箇所はGetItem メソッドに必要なパラメータを設定するための構造体です。この構造体を使用して、どのテーブルからどのキーを使ってデータを取得するかを指定します。
```
input := &dynamodb.GetItemInput{
	Key: map[string]*dynamodb.AttributeValue{
		"id": {
			S: aws.String(id),
		},
	},
	TableName: aws.String("MyDynamoDB"),
}
```
以下は`GetItemInput`で定義できる項目です。
`Key`と`TableName`は必須ですが、それ以外はオプショナルです。
今回、Keyにはidをstringで、`TableName`にはスタックを作成した時につけたテーブル名を指定します。

| フィールド名 | 型 | 説明 |必須|
|:-:|:-:|:-:|:-:|
| AttributesToGet | []*string | 古いパラメータで、代わりに ProjectionExpression を使用します。詳細は [AttributesToGet](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.AttributesToGet.html) を参照。 ||
| ConsistentRead | *bool | 読み取り一貫性モデルを決定します。true に設定すると、強い一貫性のある読み取りが使用され、それ以外の場合は最終的な一貫性のある読み取りが使用されます。 ||
| ExpressionAttributeNames | map[string]*string | 式内の属性名の置換トークン。予約語との衝突回避や式内の属性名の繰り返しの回避に使用します。詳細は [ExpressionAttributeNames](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html) を参照。 ||
| Key | map[string]*AttributeValue | 取得するアイテムのプライマリキーを表す属性名と AttributeValue オブジェクトのマップ。単純プライマリキーの場合はパーティションキーのみ、複合プライマリキーの場合はパーティションキーとソートキーの両方を提供する必要があります。 |⭕️|
| ProjectionExpression | *string | テーブルから取得する1つ以上の属性を識別する文字列。スカラー、セット、または JSON ドキュメントの要素を含むことができます。属性が指定されていない場合、すべての属性が返されます。詳細は [ProjectionExpression](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html) を参照。 ||
| ReturnConsumedCapacity | *string | レスポンスに含まれるプロビジョニングまたはオンデマンドのスループット消費の詳細レベルを決定します。INDEXES、TOTAL、NONE のいずれかを指定できます。 ||
| TableName | *string | 要求されたアイテムを含むテーブルの名前。テーブルの Amazon Resource Name (ARN) を指定することもできます。 |⭕️|

そして、この中で使用している`dynamodb.AttributeValue`はDynamoDBとやり取りする際に、DynamoDBのデータ型を表現するために使います。

例えば文字列の場合は`S: aws.String()`であったり、`BOOL: aws.Bool(true)`のように記述することでDynamoDBが認識したり、DynamoDBからの値を取得する時にAttributeValue形式で受け取ることができます。

| フィールド名 | 型 | 説明 |必須|
|:-:|:-:|:-:|:-:|
| B | []byte | バイナリ型の属性。例: "B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" ||
| BOOL | *bool | ブール型の属性。例: "BOOL": true ||
| BS | [][]byte | バイナリセット型の属性。例: "BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="] ||
| L | []*AttributeValue | リスト型の属性。例: "L": [ {"S": "Cookies"} , {"S": "Coffee"}, {"N": "3.14159"}] ||
| M | map[string]*AttributeValue | マップ型の属性。例: "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} ||
| N | *string | 数値型の属性。例: "N": "123.45"。数値は文字列として送信されますが、DynamoDBでは数値型属性として扱われます。 ||
| NS | []*string | 数値セット型の属性。例: "NS": ["42.2", "-19", "7.5", "3.14"]。数値は文字列として送信されますが、DynamoDBでは数値型属性として扱われます。 ||
| NULL | *bool | NULL型の属性。例: "NULL": true ||
| S | *string | 文字列型の属性。例: "S": "Hello" ||
| SS | []*string | 文字列セット型の属性。例: "SS": ["Giraffe", "Hippo" ,"Zebra"] ||

そして、`GetItem`は返り値として`*GetItemOutput`と`error`を受け取ります。
```
result, err := dynamoDb.GetItem(input)

if err != nil {
	return events.APIGatewayProxyResponse{
		StatusCode: http.StatusBadRequest,
		Body:       err.Error(),
	}, nil
}
```
`*GetItemOutput`では`ConsumedCapacity`と`Item`を返却します。
`ConsumedCapacity`は、操作によって消費されたプロビジョニングされたスループットの量を示します。これには、テーブル全体および関連するインデックスに対する統計情報が含まれています。この情報は、ReturnConsumedCapacity パラメータが指定された場合にのみ返されます。

`Item`は、map[string]*AttributeValue 型のフィールドで、取得したアイテムの実際のデータを含んでいます。キーは属性名（文字列）、値はその属性の値を表す AttributeValue オブジェクトです。

もし、errがnilでない場合はStatusCodeでStatusBadRequestとerrメッセージを返却します。
httpにはステータスコードやCookieのセットなどhttpに関連する便利な変数やメソッドが色々と用意されています。

https://pkg.go.dev/net/http#:~:text=Common%20HTTP%20methods

次に、以下の部分を見ていきましょう。
```
item := Item{}
err = dynamodbattribute.UnmarshalMap(result.Item, &item)
if err != nil {
	return events.APIGatewayProxyResponse{
		StatusCode: http.StatusInternalServerError,
		Body:       err.Error(),
}, nil
}
```
以下は最初に定義した構造体のインスタンスを作成しています。この構造体は、DynamoDBから取得したデータを格納するためのものです。
```
item := Item{}:
```
で、次が個人的に理解しづらかった箇所なので詳しく書きます。
```
err = dynamodbattribute.UnmarshalMap(result.Item, &item)
```
まずパッと思いついたのが`GetItem()`した`resultのItem`を以下のようにMarshalすれば良くない？ってことです。
```
body, _ := json.Unmarshal(result.Item)
```

しかし、よく考えると`result.Item`は`map[string]*dynamodb.AttributeValue`なわけです。
つまり、DynamoDBから受け取った時点ではJSON形式で表現すると以下のようなっている（はず）。
```
{
  "id": { "S": "123" },
  "name": { "S": "John Doe" },
  "age": { "N": "30" }
}
```

つまり、これを`json.Unmarshal`しようとしても`json.Unmarshalは、JSON形式のバイトスライスをGoのデータ型に変換するもの`なので、G期待したjsonにはならない訳です。というよりjson.Unmarshalではバイトスライスが期待されるのでそもそもエラーが出る。

では、どうしたら良いかというと今回のプロセスである`dynamodbattribute.UnmarshalMap(result.Item, &item)`をすることです。

dynamodbattribute.UnmarshalMapは、AttributeValuesのマップからアンマーシャルし、DynamoDB形式のデータをGoの構造体のItemに変換しています。

要は、DynamoDBのデータ形式をGoのデータ型に変換するってイメージですね。
で、エラーハンドリングは先ほどと同じなので割愛します。

ここまでをまとめると、DynamoDBにテーブルとidを指定してデータを受け取り、その受け取ったデータをGoで使えるように変換したって感じです。

そして、最後に以下のようにgoのデータ型に変換されたitemを`json.Marshal`でJSON形式のバイトスライスに変換し、StatusCodeを`http.StatisOK`とJSON形式であるbodyをstringにコンバージョンして返却している。

json.Marshal は、Goのデータ型をJSON形式のバイトスライスに変換するものです。（詳しくは以下の記事を参照ください。）
```
body, _ := json.Marshal(item)

return events.APIGatewayProxyResponse{
	StatusCode: http.StatusOK,
	Body:       string(body),
}, nil
```

https://qiita.com/YokoYokoko/items/a1a3f410ef3251f67a9c#jsonunmarshal%E3%81%A8marshal%E3%81%A8%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89:~:text=ReadAll(resp.Body)%0A%7D-,json.Unmarshal%E3%81%A8Marshal%E3%81%A8%E3%82%A8%E3%83%B3%E3%82%B3%E3%83%BC%E3%83%89,-json.Marshal%20%E3%81%AF

# POSTメソッドの実装
さて、続いてはPOSTの実装を解説していきます。
しかし、正直GETとほとんど変わらないのであまり必要ない気もしますが。。。

まず、以下の部分では引数の`request`のBodyをItem構造体に変換し、itemのメモリアドレスに値を格納しています。
そして、エラーが起きればBadRequestとエラーメッセージを返却する形です。
```
func putItem(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	item := Item{}
	err := json.Unmarshal([]byte(request.Body), &item)
	if err != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusBadRequest,
			Body:       err.Error(),
		}, nil
	}
 ```

次に以下の箇所ですが、GETの時に`GetItemInput`があったように`PutItemInput`があります。
```
	input := &dynamodb.PutItemInput{
		TableName: aws.String(tableName),
		Item: map[string]*dynamodb.AttributeValue{
			"id": {
				S: aws.String(item.ID),
			},
			"content": {
				S: aws.String(item.Content),
			},
		},
	}
	_, err = dynamoDb.PutItem(input)
```
そして、`&dynamodb.GetItemInput`を引数に渡したのと同様に`&dynamodb.PutItemInput`を引数に渡します。

`dynamodb.PutItem`は、指定されたテーブルにアイテムを追加します。既に同じプライマリキーを持つアイテムが存在する場合、そのアイテムは上書きされます。

↑ これ地味に危険ですよね。更新したと思ったら全て置き換わってる、みたいな。部分的に変えたい時は`UpdateItem`を使うっぽいです。なのでPutItemInputは更新用途よりも追加（POST）用途とした方が安全そう。

`PutItemInput`構造体も色々と設定することができますが、`AttributeValue`と`TableName`は必須項目となっています。以下はその他の設定値です。
以下に、フィールドの必須情報を含めた表を作成しました：

| フィールド名 | 型 | 説明 | 必須 |
|:-:|:-:|:-:|:-:|
| `ConditionExpression` | `*string` | 条件付き `PutItem` 操作が成功するために満たされなければならない条件。関数、比較演算子、論理演算子を含むことができる。詳細は [Condition Expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html) を参照。 | |
| `ConditionalOperator` | `*string` | レガシーパラメータ。`ConditionExpression` の代わりに使用。詳細は [ConditionalOperator](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.ConditionalOperator.html) を参照。 | |
| `Expected` | `map[string]*ExpectedAttributeValue` | レガシーパラメータ。`ConditionExpression` の代わりに使用。詳細は [Expected](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LegacyConditionalParameters.Expected.html) を参照。 | |
| `ExpressionAttributeNames` | `map[string]*string` | 式内の属性名のプレースホルダ。予約語と競合する名前や特定の文字を避けるために使用。詳細は [ExpressionAttributeNames](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html) を参照。 | |
| `ExpressionAttributeValues` | `map[string]*AttributeValue` | 式内の属性値のプレースホルダ。例: `:val`。詳細は [Condition Expressions](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.SpecifyingConditions.html) を参照。 | |
| `Item` | `map[string]*AttributeValue` | 属性名と値のペアのマップ。主キー属性は必須。他の属性も任意で提供可能。詳細は [Primary Key](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.PrimaryKey) を参照。 | ⭕️ |
| `ReturnConsumedCapacity` | `*string` | 応答に返されるプロビジョニングスループットまたはオンデマンドスループット消費の詳細レベル。`INDEXES`、`TOTAL`、`NONE` のいずれか。 | |
| `ReturnItemCollectionMetrics` | `*string` | アイテムコレクションメトリクスを返すかどうか。`SIZE` または `NONE`。 | |
| `ReturnValues` | `*string` | `PutItem` リクエストで更新される前のアイテム属性を取得するためのオプションパラメータ。`NONE` または `ALL_OLD` のいずれか。 | |
| `ReturnValuesOnConditionCheckFailure` | `*string` | 条件チェックが失敗した場合にアイテム属性を返すためのオプションパラメータ。 | |
| `TableName` | `*string` | アイテムを含むテーブルの名前。テーブルの Amazon Resource Name (ARN) も使用可能。 | ⭕️ |

この表では、必須フィールドに "⭕️" をマークし、その他のフィールドは任意としています。

そして、最後にエラー処理をして問題なければStatusCreatedを返却しています。
```
	if err != nil {
		return events.APIGatewayProxyResponse{
			StatusCode: http.StatusInternalServerError,
			Body:       err.Error(),
		}, nil
	}

	return events.APIGatewayProxyResponse{
		StatusCode: http.StatusCreated,
	}, nil
}
```

# まとめ
思っていたよりも案外簡単に書くことができました。
パッケージが充実しているので、例えば`dynamodb`で色々と用意されているので`GetItem`やら`PutItem`に`~Input`で指定されている値を期待通りに形で渡せば一応動かすことはできます。

`aws.String`やら`AttributeValue`やらちょっと慣れない書き方もありましたが使っているうちに慣れるかなという感想です。

次回はこれに更新と削除のメソッドも追加していこうと思います！