はじめに
AWS Lambda + DynamoDBでTodoリスト用REST APIを構築した際にハマったことのメモ。
構築したもの:https://github.com/r-wakatsuki/kadai4todo
ハマったこと
以下、ハマった際に遭遇した事象と解決方法を記載していく。
DynamoDBをまともに利用したのが初めてだったため、DynamoDBについてがほとんどである。
1. DynamoDBに登録するデータの値には空文字は指定できないがnullは登録可能。ただしその場合は同じプロパティにNULL型とその他の型が混在することになる。
事象
content
プロパティに空文字が指定されている場合、空文字をString型のままDynamoDBに登録しようとすると、ClientError: An error occurred (ValidationException) when calling the PutItem operation: One or more parameter values were invalid: An AttributeValue may not contain an empty string
というエラーとなる。
InputEvent:
{
"httpMethod": "POST",
"resource": "/todo",
"body": "{\"title\": \"たいとるですよ\",\"content\": \"\"}"
}
API:<Todo登録>
#〜中略〜
#要素定義:content
req_content = req_body_dict.get('content','')
if req_content == None:
req_content = ''
#〜中略〜
#<Todo登録>実行
item = {
"todoid": str(uuid.uuid4()),
"title": req_title,
"content": req_content,
"priority": req_priority,
"done": req_done,
"created_at": current_date,
"updated_at": current_date
}
table.put_item(
Item = item
)
#<Todo登録>実行結果リターン
return(return200(item))
Response:
{
"errorMessage": "An error occurred (ValidationException) when calling the PutItem operation: One or more parameter values were invalid: An AttributeValue may not contain an empty string",
"errorType": "ClientError",
"stackTrace": [
" File \"/var/task/lambda_function.py\", line 72, in lambda_handler\n Item = item\n",
" File \"/var/runtime/boto3/resources/factory.py\", line 520, in do_action\n response = action(self, *args, **kwargs)\n",
" File \"/var/runtime/boto3/resources/action.py\", line 83, in __call__\n response = getattr(parent.meta.client, operation_name)(**params)\n",
" File \"/var/runtime/botocore/client.py\", line 320, in _api_call\n return self._make_api_call(operation_name, kwargs)\n",
" File \"/var/runtime/botocore/client.py\", line 623, in _make_api_call\n raise error_class(parsed_response, operation_name)\n"
]
}
content
プロパティが空文字の場合にNULL型に変換する処理を入れ、NULL型でDynamoDBへの登録を試みると、登録処理は問題なく行われる。
InputEvent:
{
"httpMethod": "POST",
"resource": "/todo",
"body": "{\"title\": \"たいとるですよ\",\"content\": \"\"}"
}
API:<Todo登録>
#〜中略〜
#要素定義:content
req_content = req_body_dict.get('content')
if req_content == '':#空文字の場合はnull型を入れるようにする。
req_content = None
#〜中略〜
#<Todo登録>実行
item = {
"todoid": str(uuid.uuid4()),
"title": req_title,
"content": req_content,
"priority": req_priority,
"done": req_done,
"created_at": current_date,
"updated_at": current_date
}
table.put_item(
Item = item
)
#<Todo登録>実行結果リターン
return(return200(item))
Response:
{
"statusCode": 200,
"body": {
"todoid": "0b19d07a-7f41-4d58-b179-45889e236743",
"title": "たいとるですよ",
"content": null,
"priority": "normal",
"done": 0,
"created_at": "2019-09-07T22:39:00",
"updated_at": "2019-09-07T22:39:00"
}
}
しかし、DynamoDBのコンソール画面からテーブルを見ると登録したデータのcontent
プロパティの値がtrue
となっている。
さらにDynamoDBのテーブルにデータが一つも登録されていない状態であったため、content
プロパティの型がNULL
型となっている。
この状態でcontent
プロパティにいくつか値を指定して登録し、取得してみると次のような結果となった。
ユーザーが登録しようとする値(型) | DynamoDBに登録する際の値(型) | DynamoDB管理画面上の値 | 実際にDynamoDBに登録された値(型) |
---|---|---|---|
(String) | null(NULL) | true | null(NULL) |
true(String) | true(String) | true | true(String) |
本文ですよ。(String) | 本文ですよ。(String) | 本文ですよ。 | 本文ですよ。(String) |
None(String) | None(String) | None | None(String) |
要するに登録時のデータの型をDynamoDB側で保持してくれている動作となる。これでも運用できなくはないがString型とNULL型が同じプロパティに混在しているのは気持ち悪い。
解決
Todoデータの登録時や更新時にcontent
プロパティの末尾に</end>
タグを必ず付与して、String型のみを扱うようにした。
API:<Todo登録>
#〜中略〜
#要素定義:content
req_content = req_body_dict.get('content')
if req_content == None:
req_content = '</end>'
else:
req_content = req_content + '</end>'
#〜中略〜
#<Todo登録>実行
item = {
"todoid": str(uuid.uuid4()),
"title": req_title,
"content": req_content,
"priority": req_priority,
"done": int(req_done), #明示的にint型に設定
"created_at": current_date,
"updated_at": current_date
}
table.put_item(
Item = item
)
Todoデータの取得時にはcontent
属性の末尾から</end>
タグを除去する処理を入れた。
import re
API:<Todo個別取得>
##DBからTodoをtodoidで取得
todo = table.get_item(
Key={
'todoid': req_todoid
}
)['Item']
#contentから</end>タグ削除
todo['content'] = re.sub('</end>$', '', todo['content'])
2. DynamoDBにint型で登録した値が取り出し時にDecimal型になる
事象
Todoステータスdone
プロパティの値は「未完了0
」と「完了1
」の2パターンとしている。
そこでDynamoDBへのTodo登録時の処理ではTodoのdone
プロパティの値を明示的にint型に設定してDynamoDBに登録するようにした。
InputEvent:
{
"httpMethod": "POST",
"resource": "/todo",
"body": "{\"title\": \"内部定例\",\"done\": 0}"
}
API:<Todo登録>
#〜中略〜
#<Todo登録>実行
item = {
"todoid": str(uuid.uuid4()),
"title": req_title,
"content": req_content,
"priority": req_priority,
"done": int(req_done), #明示的にint型に設定
"created_at": current_date,
"updated_at": current_date
}
table.put_item(
Item = item
)
Response:
{
"todoid": "35f047ba-1121-4473-8587-b893f0938389",
"title": "内部定例",
"content": "",
"priority": "normal",
"done": 0,
"created_at": "2019-09-07T16:27:54",
"updated_at": "2019-09-07T16:27:54"
}
しかし、登録したデータをDynamoDBから取得するとdone
プロパティの値がDecimal型<class 'decimal.Decimal'>
となってしまっている。
また、取得したデータをレスポンスするためにjson.dumps
しようとするとTypeError:Object of type Decimal is not JSON serializable
となりJSONシリアライズできない。
InputEvent:
{
"httpMethod": "GET",
"resource": "/todo/{todoid}",
"pathParameters": "{\"todoid\": \"35f047ba-1121-4473-8587-b893f0938389\"}"
}
API:<Todo個別取得>
#〜中略〜
##DBからTodoをtodoidで取得
todo = table.get_item(
Key={
'todoid': req_todoid
}
)['Item']
print(type(todo['done']))#型確認
#〜中略〜
#<Todo個別取得>実行結果リターン
return{
'statusCode': 200,
'body': json.dumps(todo)
}
Fanction Logs:
<class 'decimal.Decimal'>
Response:
{
"errorMessage": "Object of type Decimal is not JSON serializable",
"errorType": "TypeError",
"stackTrace": [
" File \"/var/task/lambda_function.py\", line 209, in lambda_handler\n 'body': json.dumps(item)\n",
" File \"/var/lang/lib/python3.7/json/__init__.py\", line 231, in dumps\n return _default_encoder.encode(obj)\n",
" File \"/var/lang/lib/python3.7/json/encoder.py\", line 199, in encode\n chunks = self.iterencode(o, _one_shot=True)\n",
" File \"/var/lang/lib/python3.7/json/encoder.py\", line 257, in iterencode\n return _iterencode(o, 0)\n",
" File \"/var/lang/lib/python3.7/json/encoder.py\", line 179, in default\n raise TypeError(f'Object of type {o.__class__.__name__} '\n"
]
}
解決
json.dumps
時にDecimal型の値があればint型に変換するようにした。
InputEvent:
{
"httpMethod": "GET",
"resource": "/todo/{todoid}",
"pathParameters": "{\"todoid\": \"35f047ba-1121-4473-8587-b893f0938389\"}"
}
API:<Todo個別取得>
##DBからTodoをtodoidで取得
todo = table.get_item(
Key={
'todoid': req_todoid
}
)['Item']
#〜中略〜
#Decimal型の場合はint型に変換
def decimal_default_proc(obj):
if isinstance(obj, Decimal):
return int(obj)
raise TypeError
#<Todo個別取得>実行結果リターン
return{
'statusCode': 200,
'body': json.dumps(todo,default=decimal_default_proc)
}
Response:
{
"todoid": "35f047ba-1121-4473-8587-b893f0938389",
"title": "内部定例",
"content": "",
"priority": "normal",
"done": 0,
"created_at": "2019-09-07T16:27:54",
"updated_at": "2019-09-07T16:27:54"
}
上記ではDecimal -> int変換用の関数を利用しているが、関数を使わなくてもDecimal型になることが事前に分かっている値は個別にint(値)
すればよい。
参考
https://qiita.com/ekzemplaro/items/5fa8900212252ab554a3
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.Python.03.html
3.DynamoDBに登録されるデータのプロパティの並び順を定義できない。
事象
例えば、以下のようにプロパティの順番を整列させたTodoをDynamoDBに登録する。
API:<Todo登録>
#〜中略〜
#<Todo登録>実行
item = {
"todoid": str(uuid.uuid4()),
"title": req_title,
"content": req_content,
"priority": req_priority,
"done": req_done,
"created_at": current_date,
"updated_at": current_date
}
table.put_item(
Item = item
)
登録したデータをDynamoDBから取得するとプロパティの並び順が整列されていない。
API:<Todo個別取得>
##DBからTodoをtodoidで取得
todo = table.get_item(
Key={
'todoid': req_todoid
}
)['Item']
#〜中略〜
#<Todo個別取得>実行結果リターン
return{
'statusCode': 200,
'body': json.dumps(todo)
}
Response:
{
"updated_at": "2019-09-01T21:38:37",
"content": "定期券の更新ができた",
"todoid": "6f9cd0e6-fbf0-48be-ac64-b4763c482d4d",
"created_at": "2019-09-01T20:32:30",
"priority": "normal",
"done": 1,
"title": "定期更新"
}
DynamoDB自体の設定でもプロパティの並び順をあらかじめ定義する設定は見当たらない。
解決
取得後にプロパティの並び順を整列する処理を入れる。
API:<Todo個別取得>
##DBからTodoをtodoidで取得
todo = table.get_item(
Key={
'todoid': req_todoid
}
)['Item']
#〜中略〜
#プロパティ並び順整列
item = {
"todoid": todo['todoid'],
"title": todo['title'],
"content": todo['content'],
"priority": todo['priority'],
"done": todo['done'],
"created_at": todo['created_at'],
"updated_at": todo['updated_at']
}
#<Todo個別取得>実行結果リターン
return{
'statusCode': 200,
'body': json.dumps(item)
}
Response:
{
"todoid": "6f9cd0e6-fbf0-48be-ac64-b4763c482d4d",
"title": "定期更新",
"content": "定期券の更新ができた",
"priority": "normal",
"done": 1,
"created_at": "2019-09-01T20:32:30",
"updated_at": "2019-09-01T21:38:37"
}
以上