LoginSignup
5
5

More than 3 years have passed since last update.

【Python】AWS Lambda + DynamoDBでTodoリスト用REST APIを構築した際にハマったこと

Last updated at Posted at 2019-09-09

はじめに

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型となっている。

image.png

この状態で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)

image.png

要するに登録時のデータの型を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"
}

以上

5
5
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
5
5