🌊

DynamoDBのupdate条件式でハマるポイント

に公開

1. 結論(この記事で得られること)

DynamoDBのUpdateItemで条件式を書くとき、「なぜかエラーになる」「思った条件で更新されない」という経験、ありませんか?

私も3年前、本番環境で条件式のミスによる予期しない更新を起こしかけて、コードレビューで先輩に止められた経験があります。

この記事では以下が得られます:

  • 条件式(ConditionExpression)の正しい書き方と頻出エラーパターン
  • UpdateExpressionとConditionExpressionの使い分け
  • 属性が存在しない場合の安全な扱い方
  • AIを使った高速デバッグ手法(有料部分)
  • 本番運用での失敗パターンと対策(有料部分)

10分で読めて、明日から実務で使える内容に絞りました。

2. 前提(環境・読者層)

想定読者

  • DynamoDBを触り始めて数ヶ月〜1年程度
  • UpdateItemの基本は知っているが、条件式で詰まったことがある
  • 本番環境でのミスを未然に防ぎたいエンジニア

動作環境

  • AWS SDK for JavaScript v3(コード例はTypeScript)
  • DynamoDB(リージョン・容量モードは問わず)
  • Node.js 18以上推奨

boto3やJavaのSDKでも考え方は共通です。

3. Before:よくあるつまずきポイント

3-1. 「attribute_exists」と「attribute_not_exists」の罠

// ❌ NG:よくある間違い
await docClient.update({
  TableName: 'Users',
  Key: { userId: 'user123' },
  UpdateExpression: 'SET #status = :newStatus',
  ConditionExpression: 'attribute_exists(#status)', // 既存属性がある場合のみ更新
  ExpressionAttributeNames: {
    '#status': 'status'
  },
  ExpressionAttributeValues: {
    ':newStatus': 'active'
  }
});

何がダメか?

  • 「attribute_exists」は「その属性がitemに存在するか」をチェックする
  • 初回登録時やステータス未設定のユーザーでは更新が失敗する
  • 「ConditionalCheckFailedException」が投げられるが、原因がログからわかりにくい

私も昔、これで「特定ユーザーだけ更新できない」というバグを半日追いかけました。

3-2. 比較演算子での型不一致

// ❌ NG:数値比較のつもりが文字列比較になっている
ConditionExpression: '#version = :expectedVersion',
ExpressionAttributeValues: {
  ':expectedVersion': '5' // ← DynamoDB上はNumber型なのに文字列で指定
}
  • エラーにはならないが、条件が常にfalseになり更新が通らない
  • 型の不一致は実行時まで分からない(TypeScriptでも防げない)

3-3. 論理演算の優先順位ミス

//NG:意図しない評価順序
ConditionExpression: '#status = :active OR #status = :pending AND #retryCount < :maxRetry'
//ANDが先に評価されるため、意図と異なる動作になる

これも実際にレビューで指摘したケースです。括弧を使わないと事故ります。

4. After:基本的な解決パターン

4-1. 属性の存在チェックを正しく行う

//OK:属性がない場合はデフォルト値で作成、ある場合は条件付き更新
await docClient.update({
  TableName: 'Users',
  Key: { userId: 'user123' },
  UpdateExpression: 'SET #status = :newStatus, #updatedAt = :now',
  ConditionExpression: 'attribute_not_exists(#status) OR #status = :oldStatus',
  ExpressionAttributeNames: {
    '#status': 'status',
    '#updatedAt': 'updatedAt'
  },
  ExpressionAttributeValues: {
    ':newStatus': 'active',
    ':oldStatus': 'pending',
    ':now': Date.now()
  }
});

ポイント

  • 「OR」で「新規作成」と「既存更新」の両方に対応
  • 楽観ロック的な使い方(期待する古い値をチェック)

4-2. 型を明示的に統一する

// ✅ OK:数値型として明示
ExpressionAttributeValues: {
  ':expectedVersion': 5,  // Numberとして扱われる
  ':maxRetry': 3
}

AWS SDK v3では型推論が効くが、念のため確認

import { marshall } from '@aws-sdk/util-dynamodb';
 
// 型が不安なら明示的に変換
const values = marshall({
  ':expectedVersion': 5
}, { removeUndefinedValues: true });

4-3. 論理演算は括弧で明示

//OK:意図通りの評価順序
ConditionExpression: '(#status = :active OR #status = :pending) AND #retryCount < :maxRetry'

ここは必ずレビューでチェックします。自分も見落としやすいので。

4-4. よく使う安全パターン集

// パターン1:楽観ロック(バージョン番号チェック)
ConditionExpression: '#version = :currentVersion',
UpdateExpression: 'SET #data = :newData, #version = #version + :inc',
ExpressionAttributeValues: {
  ':currentVersion': 5,
  ':newData': { /* ... */ },
  ':inc': 1
}
 
// パターン2:在庫減算(負にならないチェック)
ConditionExpression: '#stock >= :quantity',
UpdateExpression: 'SET #stock = #stock - :quantity'
 
// パターン3:初回のみ作成(既存なら何もしない)
ConditionExpression: 'attribute_not_exists(PK)'

特に在庫管理などの数値操作では、条件式が必須です。


続きはnoteで

この記事の実装編・詳細解説はnoteで公開しています。

実際のコード例や、実務で遭遇するハマりポイントなど、より踏み込んだ内容を書いています。

Discussion