[備忘録] boto3.dynamodb.conditons.Keyとboto3.dynamodb.conditions.Attrについて

2020.06.06

boto3において、DynamoDB conditionsというものがある。
QueryUpdateItemなどで"ConditionExpression"に渡す条件の記述に使用する。

使い方

Attr('id').eq('E1EE1703-0AA6-4EFF-A1E9-CBA15991B6B4')

上記例では、DynamoDB TableのItemのidというAttributeにE1EE1703-0AA6-4EFF-A1E9-CBA15991B6B4と等しい文字列が入っているという条件になる。

上記例では、Attrを使用したがKeyでも基本的な使い方は変わらない。
また、KeyよりもAttrの方が条件を記述するeq()などの関数の種類が多い (詳しくはドキュメントを見てほしい)。

また、論理和(or)と論理積(and)の記述方法は次のようになっている。

# 論理和 (or)
Attr('type').eq('router') | Attr('type').eq('firewall')

# 論理積 (and)
Attr('type').eq('router') | Attr('vendor').eq('cisco')

KeyAttrの違い

結論から言うと、正直わからなかった。
たぶん、PartitionKeySortKeyにはKeyを、それ以外にはAttrを使う想定ではないかと予測するが、確証はない。

Pythonのコードを見る限りでは、Attrの方がKeyよりも条件を記述するための関数が多いだけで、他の違いを見受けられない。

JSON化するにあたって

現在参画しているプロジェクトではDynamoDBへの操作失敗時に、操作に使用した関数に渡した引数をログに出力している。
また、ログ出力に関してはJSON化を行っている。

しかし、JSON化が不可能な型(class)に関しては文字列化しているだけである。 このとき、KeyAttrを使って記述した条件は次のような文字列になる。

'<boto3.dynamodb.conditions.Equals object at 0x10aae33c8>'

そのため、このままではどのような条件なのかがわからない。

JSON化したログに条件を残すためには、json.dumps()defaultという引数に次の関数を渡すことで多少見にくいが、条件の情報を残すことができた。

def convert(obj):
    if isinstance(obj, ConditionBase):
        return obj.get_expression()
    if isinstance(obj, AttributeBase):
        return obj.name

option = {
    'Condition': Key('id').eq(100)
}
text = json.dumps(option, default=convert, indent=2)
# {
#   "Condition": {
#     "format": "{0} {operator} {1}",
#     "operator": "=",
#     "values": [
#       "id",
#       100
#     ]
#   }
# }

以下、これを見つけるために行った調査の内容について記述する。

まず、調査にあたってはipythonを使用した。
具体的には、ipythonであたりをつけつつ、ソースコードの情報も加味して、JSON化に必要な手段を調べた。

KeyAttrを使って記述した条件は、原則ConditionBaseというクラスを継承したクラスのオブジェクトであった。
直接継承してるのもあれば、先祖をたどるとConditionBaseがあったってのもある。

この、ConditionBaseにはget_expression()という関数がありformat/operator/valuesという3つのキーを持つdictを返す。

  • format: 条件式。Pythonのformat()やf-stringsの記述方法に似ている。
  • operator: 条件の種類を示す、と思う。=とかORとか。
  • values: 条件式で使用する値。配列で入っている。

だいたい、こんな役割があって、ipythonで見るとこんな感じだった。

In [11]: a = Key('id').eq(100)

In [12]: a.get_expression()
Out[12]:
{'format': '{0} {operator} {1}',
 'operator': '=',
 'values': (<boto3.dynamodb.conditions.Key at 0x10ab318d0>, 100)}
In [14]: b = Key('timestamp').between(10, 20)

In [15]: b.get_expression()
Out[15]:
{'format': '{0} {operator} {1} AND {2}',
 'operator': 'BETWEEN',
 'values': (<boto3.dynamodb.conditions.Key at 0x10ab313c8>, 10, 20)}
In [16]: c = Key('type').eq('device') & Key('name').eq('test')

In [17]: c.get_expression()
Out[17]:
{'format': '({0} {operator} {1})',
 'operator': 'AND',
 'values': (<boto3.dynamodb.conditions.Equals at 0x10aaeb630>,
  <boto3.dynamodb.conditions.Equals at 0x10aaeba20>)}
In [18]: d = Attr('isDelete').exists()

In [19]: d.get_expression()
Out[19]:
{'format': '{operator}({0})',
 'operator': 'attribute_exists',
 'values': (<boto3.dynamodb.conditions.Attr at 0x10a93fa90>,)}

これらを見る限り、valuesにはKeyAttr, ConditionBaseを継承したクラスのオブジェクト、実際の値が入ることがわかる。

この段階で、ConditionBaseを継承したクラスのオブジェクトであればget_expression()を実行すれば、ほとんどの情報を取ることができるとわかった。

あとは、KeyAttrをどうするのか。
KeyAttrAttributeBaseというクラスを継承していた。
AttributeBaseにはnameというプロパティがあり、オブジェクトを作成する際に渡している引数が入っている。
(Key('name') だとすると、'name'が入っている)

どのAttributeか区別できれば良いと考えれば、AttributeBaseを継承したクラスのオブジェクトはnameを出力されば良い。
もし、KeyなのかAttrなのかまで区別しようと思えば、json.dumpsdefaultに渡す関数を次のようにすれば区別できるだろう。

def convert(obj):
    if isinstance(obj, ConditionBase):
        return obj.get_expression()
    if isinstance(obj, Attr):
        return f'Attr("{obj.name}")'
    if isinstance(obj, Key):
        return f'Key("{obj.name}")'