DynamoDBでプライマリーキーではない属性を一意に保つ方法
2022.05.30
Amazon DynamoDBにおいて、プライマリーキーに指定していない属性が重複しないようにする方法を紹介します。
実際に動作するPythonのコードも載せていますので、ぜひご覧ください。
はじめに
サービス開発上、プライマリーキーではない属性に一意性を持たせたい状況は多々存在します。例えば、ユーザーのメールアドレスをログイン時に使用する場合、メールアドレスの重複が起こってしまうと、他人のアカウントにログインできてしまうといった危険性が生じるため、この問題に対処する必要があります。
MySQLやPostgreSQLといった主なRDB(リレーショナルデータベース)には、ユニーク制約といって、プライマリーキーではない属性の一意性を保証する機能があります。しかし、DynamoDBにはそのような機能を提供していないため、実装側で仕組みを作る必要があります。
本記事では、Amazon Web Services(以下、AWS)の提供するDynamoDBにおいて、プライマリーキーではない属性を一意に保つ実装の方法をご紹介します。
> AWS利用料 今なら8%OFF
一意性の保てないNGパターン
例えば、以下のようなユーザーを管理するテーブルがあると仮定します。
| PK | username | |
|---|---|---|
| USER#92d088ba-8132-4f8b-adad-6894322ed9aa | taro | taro@example.com |
PKをパーティションキーとして設定しており、プライマリーキーはこの一つのパーティションキーで構成されます。
そして、以下のようなコードでユーザーの新規作成を行おうとしています。
from uuid import uuid4
import boto3
client = boto3.client('dynamodb')
TABLE_NAME = 'user’
EMAIL_INDEX_NAME = 'email-index'
def does_email_exist(email):
response = client.query(
TableName=TABLE_NAME,
IndexName=EMAIL_INDEX_NAME,
KeyConditionExpression='email = :email',
ExpressionAttributeValues={
':email': {'S': email}},
)
return len(response['Items']) > 0
def put_user(username, email):
client.put_item(
TableName=TABLE_NAME,
Item={
'PK': {'S': f'USER#{uuid4()}'},
'username': {'S': username},
'email': {'S': email}}
)
def main():
username = 'taro'
email = 'taro@example.com'
if does_email_exist(email):
print('The email is already in use.')
return
put_user(username, email)
main()
このコードでは、ユーザーの作成(put_user())を行う前にメールアドレスの重複を防ぐために存在確認(does_email_exist())をしています。
一見問題なく思えるコードですが、メールアドレスの存在を確認し、ユーザーのレコードを書き込むまでの間に、別スレッドで対象のメールアドレスをもつユーザーのレコードが書き込まれた場合、テーブル内で重複したメールアドレスが存在してしまいます。
また、条件付き書き込みを使用する方法も考えられますが、条件に使用できる属性はプライマリーキーに限られるため解決に至りません。
次のセクションでは、メールアドレスの一意性が保証されるユーザーの追加の方法をご紹介します。
もうひとつレコードを作ることで値の重複を防ぐ
ユーザー作成の際に、ユーザーのレコードに加え、emailをプライマリーキーにもつレコードを作成することで、メールアドレスの重複を防ぐことができます。
具体的には、トランザクションを用いて、ユーザーのレコードの書き込みとメールアドレスのレコードの書き込みを一つのスレッドで行います。この際、メールアドレスのレコードは「PK(メールアドレス)が存在しない」時のみ書き込みできるように条件付けを行います。
DynamoDBは既に存在するプライマリーキーで書き込みを行う際に上書きする仕様のため、既に同じメールアドレスが存在している場合、「PKが存在している」ということでエラーを発生させ、ユーザー作成の際にメールアドレスの重複を防ぐことができます。
前のセクションの例を踏襲すると、以下のようなテーブルになるでしょう。
| PK | username | |
|---|---|---|
| USER#92d088ba-8132-4f8b-adad-6894322ed9aa | taro | taro@example.com |
| EMAIL#taro@example.com |
注意点
注意点として、メールアドレスのレコードはそのメールアドレスをもっているユーザーに対する変更に対応しなければなりません。そのため、ユーザーのレコードの追加の他にも、削除やメールアドレスの変更の際に、同様の方法でメールアドレスのレコードに変更を加える必要があります。
次のセクションでは、実際に動作するコードを載せています。
Pythonを使用した実装例
このセクションではPythonとBoto3を用いて、前セクションで説明した方法でユーザーを新規作成するコードを示します。
なお、以下の環境でコーディングを行っています。
| 対象 | バージョン |
|---|---|
| Python | 3.9.5 |
| Boto3 | 1.20.7 |
from uuid import uuid4
import boto3
client = boto3.client('dynamodb')
TABLE_NAME = ‘user’
EMAIL_INDEX_NAME = ‘email-index’
def put_user(username, email):
client.transact_write_items(
TransactItems=[
{
'Put': {
'TableName': TABLE_NAME,
'Item': {
'PK': {'S': f'USER#{uuid4()}'},
'username': {'S': username},
'email': {'S': email}}
},
},
{
'Put': {
'TableName': TABLE_NAME,
'Item': {
'PK': {'S': f'EMAIL#{email}'}},
'ConditionExpression': 'attribute_not_exists(PK)',
}
}])
def main():
username = 'taro'
email = 'taro@example.com'
try:
put_user(username, email)
except client.exceptions.TransactionCanceledException:
print('The email is already in use.')
main()
条件付き書き込みの条件に反していた場合、TransactionCanceledExceptionというエラーが生じるため、それをキャッチしてメールアドレスが既に存在していた場合の分岐処理を行なっています。
まとめ
今回はAWSのDynamoDBにて、プライマリーキーではない属性を一意に保つ方法をご紹介しました。
DynamoDBにおいて一意性を保つことは不可能ではないとはいえ、対象となる属性が増えるほどコードが煩雑になったり、途中からこの仕様にすることが難しかったりするため、DBの選定やテーブル設計の段階からこの問題を意識しておく必要があると感じます。
そして、最後に宣伝にはなりますが、弊社はAWSの監視・運用代行サービスをご提供しております。 ご興味のある方はこちらをご覧ください。
ご覧いただきありがとうございました。
> AWS利用料 今なら8%OFF



