AWS SDK for JavaScript v3 を使って、 DynamoDB から指定したパーティションキーのアイテムを削除してみた

2022.10.24

こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。

最近、AWS SDK for JavaScript v3を利用してDynamoDBの操作をしているのですが、指定したパーティションキーのアイテムを削除する機会があったので、やり方をまとめておきたいと思います。

前提条件

今回試した環境は以下のとおりです。

  • @aws-sdk/client-dynamodb
    • ^3.121.0"

また、DynamoDB上のテーブルは、以下のようなテーブルを前提とします。

キー 名前 備考
パーティションキー id (String)
ソートキー type (String)
属性 cap (Number)
ローカルセカンダリインデックス(LSI) IdTypeIndex パーティションキー:id (String), ソートキー:type (String)

やりたいこと

今回やりたいことは、前述のような「パーティションキー」と「ソートキー」の両方で「プライマリーキー」を構成しているテーブルにおいて、「パーティションキー」を指定することで、複数アイテムを削除したいと思います。

例えば、以下のようなアイテムがあった場合に、id51f66320-7233-48e9-a718-733735da4249である、4つのアイテムを削除したいと考えています。

id (パーティションキー) type (ソートキー) cap
51f66320-7233-48e9-a718-733735da4249 White Mage 80
51f66320-7233-48e9-a718-733735da4249 Scholar 80
51f66320-7233-48e9-a718-733735da4249 Astrologian 80
51f66320-7233-48e9-a718-733735da4249 Sage 80
a9a7b81a-8d7d-44c1-bd98-a6a3a61a4abc White Mage 90
a9a7b81a-8d7d-44c1-bd98-a6a3a61a4abc Scholar 90
a9a7b81a-8d7d-44c1-bd98-a6a3a61a4abc Astrologian 90
a9a7b81a-8d7d-44c1-bd98-a6a3a61a4abc Sage 90

そもそも、どのAPIを利用して削除すればよいのか

上記のようなケースでは、どのAPIを利用して削除すればよいのでしょうか?

結論としては、以下となりました。

  1. QueryCommand を利用して、指定した「パーティションキー」を持つアイテムを取得する
  2. 取得したアイテムの「パーティションキー」と「ソートキー」を利用してBatchWriteItemCommandを発行し、該当アイテムを削除する

少し細かく解説していきます。

DeleteItemCommand は使えるか?

まず初めに、公式ドキュメントで、アイテムの削除に関するページを探すと以下のページが見つかりました。

ここではDeleteItemCommandを利用したアイテムの削除が紹介されていますが、一方で以下のような注記が記載されています。

次のコード例では、-KEY_NAME-パーションとソートキーの両方ではなく、パーティションキーのみで構成されたプライマリキーを持つアイテムを削除します 表にパーティションキーとソートキーで構成されるプライマリキーがある場合は、ソートキーの名前と属性も指定する必要があります。

つまり、DeleteItemCommandを利用する場合、今回のような「パーティションキー」と「ソートキー」でプライマリーキーが構成されている場合には、明示的に両方のキーと値を指定する必要が出てきます。

今回のケースだと、4回DeleteItemCommandを発行する必要が出てきます。アイテムの数が少なければあり得るかもしれませんが、アイテム数が多い場合にはリクエスト回数が増えるので避けたいところです。

一括処理のための BatchWriteItemCommand がある

今回のような一括で処理を行うケースのために、BatchWriteItemCommandというコマンドがあります。

このコマンドを利用することで一気にアイテムを削除することがあります。ただ、利用する際には、やはり「パーティションキー」と「ソートキー」が必要となってくるので、事前に該当のキー値を取得する必要があります。

(再掲)結局どうすればいい?

つまり、以下のようにすることで対応が可能となります。

  1. QueryCommand を利用して、指定した「パーティションキー」を持つアイテムを取得する
  2. 取得したアイテムの「パーティションキー」と「ソートキー」を利用してBatchWriteItemCommandを発行し、該当アイテムを削除する

やってみる

QueryCommand

QueryCommandについては、基本的には以前に下記エントリで記載したものと同様です。

下記のようなコードで、特定のidをキーとしてテーブルにクエリを発行し、アイテムを取得します。

import {
  AttributeValue,
  BatchWriteItemCommand,
  BatchWriteItemCommandInput,
  BatchWriteItemCommandOutput,
  DynamoDBClient,
  QueryCommand,
  QueryCommandInput,
  QueryCommandOutput,
  WriteRequest,
} from '@aws-sdk/client-dynamodb'
import { marshall } from '@aws-sdk/util-dynamodb'

export type Item = {
  [key: string]: AttributeValue
}

export function toDynamoDbItem<T>(t: T): Item {
  const item = marshall(t)
  return item
}

export async function sleep(msec: number): Promise<unknown> {
  return new Promise((resolve) => {
    setTimeout(resolve, msec)
  })
}

const dynamodb = new DynamoDBClient({
  apiVersion: '2012-08-10',
})

function toExpressionAttributeValues(attributes): Item {
  const exAttributes = {}

  for (const [key, value] of Object.entries(attributes)) {
    exAttributes[':' + key] = value
  }

  const expressionAttributeValues: Item = toDynamoDbItem(exAttributes)

  return expressionAttributeValues
}

async function query(input: QueryCommandInput): Promise<QueryCommandOutput> {
  const command: QueryCommand = new QueryCommand(input)
  const output: QueryCommandOutput = await dynamodb.send(command)

  return output
}

export async function queryItems(tableName: string, id: string): Promise<QueryCommandOutput> {
  let result: QueryCommandOutput = null

  const queryAttributes = {
    id: id,
  }

  const queryCommandInput: QueryCommandInput = {
    TableName: tableName,
    KeyConditionExpression: 'id = :id',
    ExpressionAttributeValues: toExpressionAttributeValues(queryAttributes),
  }

  const recursiveQuery = async (queryCommandInput: QueryCommandInput) => {
    const response: QueryCommandOutput = await query(queryCommandInput)
    if (!result) {
      result = response
    } else {
      result.Items.push(...response.Items)
    }

    // 追加Scanデータがある場合には、追加Scanを実施する(Scan結果が1MBを超えるケース)
    // ref.) https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Scan.html
    if (response.LastEvaluatedKey) {
      // APIリクエストレートを制御する
      await sleep(500)

      queryCommandInput.ExclusiveStartKey = response.LastEvaluatedKey
      await recursiveQuery(queryCommandInput)
    }
  }
  await recursiveQuery(queryCommandInput)

  // 複数回Scanを考慮して手動設定する
  result.Count = result.Items.length

  return result
}

これにより、テーブルから条件に一致するアイテムが取得できるので、以下のようにtableItemsとしてArrayオブジェクトに変換します。

export interface RoleTableKey {
  id: string
  type: string
}

export function fromDynamoDbItems<T>(items: Item[]): T[] {
  const ret: T[] = items.map((item: Item) => fromDynamoDbItem<T>(item))
  return ret
}

const queryResult: QueryCommandOutput = await queryItems(tableName, id)
const tableItems: RoleTableKey[] = fromDynamoDbItems<RoleTableKey>(queryResult.Items)

BatchWriteItemCommand

次にBatchWriteItemCommandについてです。

async function batchWriteItem(input: BatchWriteItemCommandInput): Promise<BatchWriteItemCommandOutput> {
  const command: BatchWriteItemCommand = new BatchWriteItemCommand(input)
  const output: BatchWriteItemCommandOutput = await dynamodb.send(command)

  return output
}

function getDeleteRequests(tableItems: RoleTableKey[]) {
  const deleteRequests: WriteRequest[] = []

  tableItems.forEach((item: RoleTableKey) => {
    deleteRequests.push({
      DeleteRequest: {
        Key: toDynamoDbItem({
          id: item.id,
          type: item.type,
        }),
      },
    })
  })

  return deleteRequests
}

export async function deleteTableItems(
  tableName: string,
  tableItems: RoleTableKey[]
): Promise<BatchWriteItemCommandOutput> {
  const deleteRequests: WriteRequest[] = getDeleteRequests(tableItems)
  const input: BatchWriteItemCommandInput = {
    RequestItems: {
      [tableName]: deleteRequests,
    },
  }

  const result: BatchWriteItemCommandOutput = await batchWriteItem(input)

  return result
}

こちらは、まずはBatchWriteItemCommandInputにわたすWriteRequest配列を生成します。

そして、WriteRequest配列ができたら、これをBatchWriteItemCommandInputに設定してBatchWriteItemCommandに引き渡しています。

こうすることで、生成したリクエスト対象のアイテムをバッチ処理で一括削除することができます。

まとめ

以上、AWS SDK for JavaScript v3 を使って、 DynamoDB から指定したパーティションキーのアイテムを削除してみました。

一旦Queryコマンドを発行してキー情報を取得する必要がありますが、RDBなどにおけるWhere句を利用した削除処理と異なるので、ここが少し難しいポイントかなと思います。

どなたかのお役に立てば幸いです。それでは!

参考

関連エントリ