0
0

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

Posted at

さて、今回は以下のようにファイルを記述して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で定義できる項目です。
KeyTableNameは必須ですが、それ以外はオプショナルです。
今回、Keyにはidをstringで、TableNameにはスタックを作成した時につけたテーブル名を指定します。

フィールド名 説明 必須
AttributesToGet []*string 古いパラメータで、代わりに ProjectionExpression を使用します。詳細は AttributesToGet を参照。
ConsistentRead *bool 読み取り一貫性モデルを決定します。true に設定すると、強い一貫性のある読み取りが使用され、それ以外の場合は最終的な一貫性のある読み取りが使用されます。
ExpressionAttributeNames map[string]*string 式内の属性名の置換トークン。予約語との衝突回避や式内の属性名の繰り返しの回避に使用します。詳細は ExpressionAttributeNames を参照。
Key map[string]*AttributeValue 取得するアイテムのプライマリキーを表す属性名と AttributeValue オブジェクトのマップ。単純プライマリキーの場合はパーティションキーのみ、複合プライマリキーの場合はパーティションキーとソートキーの両方を提供する必要があります。 ⭕️
ProjectionExpression *string テーブルから取得する1つ以上の属性を識別する文字列。スカラー、セット、または JSON ドキュメントの要素を含むことができます。属性が指定されていない場合、すべての属性が返されます。詳細は ProjectionExpression を参照。
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は返り値として*GetItemOutputerrorを受け取ります。

result, err := dynamoDb.GetItem(input)

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

*GetItemOutputではConsumedCapacityItemを返却します。
ConsumedCapacityは、操作によって消費されたプロビジョニングされたスループットの量を示します。これには、テーブル全体および関連するインデックスに対する統計情報が含まれています。この情報は、ReturnConsumedCapacity パラメータが指定された場合にのみ返されます。

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

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

次に、以下の部分を見ていきましょう。

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.Itemmap[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

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構造体も色々と設定することができますが、AttributeValueTableNameは必須項目となっています。以下はその他の設定値です。
以下に、フィールドの必須情報を含めた表を作成しました:

フィールド名 説明 必須
ConditionExpression *string 条件付き PutItem 操作が成功するために満たされなければならない条件。関数、比較演算子、論理演算子を含むことができる。詳細は Condition Expressions を参照。
ConditionalOperator *string レガシーパラメータ。ConditionExpression の代わりに使用。詳細は ConditionalOperator を参照。
Expected map[string]*ExpectedAttributeValue レガシーパラメータ。ConditionExpression の代わりに使用。詳細は Expected を参照。
ExpressionAttributeNames map[string]*string 式内の属性名のプレースホルダ。予約語と競合する名前や特定の文字を避けるために使用。詳細は ExpressionAttributeNames を参照。
ExpressionAttributeValues map[string]*AttributeValue 式内の属性値のプレースホルダ。例: :val。詳細は Condition Expressions を参照。
Item map[string]*AttributeValue 属性名と値のペアのマップ。主キー属性は必須。他の属性も任意で提供可能。詳細は Primary Key を参照。 ⭕️
ReturnConsumedCapacity *string 応答に返されるプロビジョニングスループットまたはオンデマンドスループット消費の詳細レベル。INDEXESTOTALNONE のいずれか。
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やらちょっと慣れない書き方もありましたが使っているうちに慣れるかなという感想です。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0