DynamoDBからの値を取得するとき、filterが効かない問題を解決する

## はじめに
業務でDynamoDBを使う中で、filterが効かない（あるはずのデータが取得できない）という現象に遭遇し、その解決に時間を要したので、備忘として対応方法を記しておきます。

### 前提
プロジェクトではAppSync、Amplify、GraphQLを使用しており、言語はNode.js、TypeScriptおよび、Expressを使用していました。

## 対応方法

### 概要
DynamoDBに項目が多数登録されている場合、`nextToken`を使って取得処理を繰り返し実行する。

どういうことかというと、DynamoDBのクエリでのデータ取得には上限（最大 1 MB のデータ）があり、一度にすべてのデータを取得することができません。

そのため、次のデータがあるかどうかの情報にあたる`nextToken`をチェックし、データがある場合には再度取得処理を実行する必要があります。

### 実装
実際のソースを確認します。
```typescript:index.ts
import express from "express";
import { generateClient } from "aws-amplify/api";
import { Amplify } from "aws-amplify";
import config from "./aws-exports";
import { listTodos } from "./graphql/queries";

// Amplifyの設定
Amplify.configure(config);
const client = generateClient();

const app = express();
const port = 3000;

// 型定義
interface Todo {
  id: string;
  name: string;
  description?: string;
}

interface ListTodosResponse {
  data: {
    listTodos: {
      items: Todo[];
      nextToken: string | null;
    };
  };
}

// ユーティリティ関数
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

app.get("/todos", async (req, res) => {
  try {
    const nameFilter = (req.query.name as string) || "";

    let items: Todo[] = [];
    let nextToken: string | null = null;

    // はじめの1回は必ず実行、以降はnextTokenがある限りループ
    do {
      const response = (await client.graphql({
        query: listTodos,
        variables: {
          filter: nameFilter
            ? {
                name: { contains: nameFilter },
              }
            : undefined,
          nextToken: nextToken,
        },
      })) as ListTodosResponse;

      const result = response.data.listTodos;
      items = items.concat(result.items);

      // nextTokenを取得
      nextToken = result.nextToken;

      await sleep(100);
    } while (nextToken);

    res.send(`<pre>items: ${JSON.stringify(items, null, 2)}</pre>`);
  } catch (error) {
    res.status(500).json({
      success: false,
      message: "Error fetching data",
      error: error,
    });
  }
});
```

`do...while`に定義しているのが、`nextToken`を利用した繰り返し処理です。
一度の処理ではデータを取得しきれないため、次のデータがあるかどうかを確認して、あればデータを取得し続けています。

### 取得例
DynamoDBに、200件のデータを登録してその結果を確認します。
`name`属性にはTodo 1からTodo 200までのデータが連番で記録されています。

`20`が含まれるデータを取得するとした場合、想定される取得結果はTodo 20、Todo 120、Todo 200の3件です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/fa0d1117-034d-3b27-eb17-17e3d28d6735.png)

想定通り、3件のデータが取得できていることがわかります。
[おまけ](#おまけ)にて間違った実装も紹介しているので、よければ見てみてください。

## まとめ
DyanamoDBは普段まったく触らないので、存在するはずのデータが取得できないという事象に遭遇したときは、解決方法の見当すらつかず、かなり調査に時間を要する事になりました。

同じような悩みを抱えている人にとってお役に立てれば幸いです。

## おまけ：間違った実装
私がデータ取得できなかったときは、ループ処理をしない実装になっていました。

```typescript:index.ts
// import文などは省略

app.get("/badTodos", async (req, res) => {
  try {
    const nameFilter = (req.query.name as string) || "";

    // ループ処理なし
    const response = (await client.graphql({
      query: listTodos,
      variables: {
        filter: nameFilter
          ? {
              name: { contains: nameFilter },
            }
          : undefined,
      },
    })) as ListTodosResponse;

    const items = response.data.listTodos.items;

    res.send(`<pre>items: ${JSON.stringify(items, null, 2)}</pre>`);
  } catch (error) {
    res.status(500).json({
      success: false,
      message: "Error fetching data",
      error: error,
    });
  }
});

// サーバー起動
app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});
```

これでも一見取得できそうですが、実際には取得結果は以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/88edd438-2aab-14a6-a9bb-12ff28ec7df3.png)

格納しているデータは正しい実装のときと同じですが、1件しか取得できていません。
これは、一番最初に取得したデータ内でたまたま条件と合致するものが含まれていたので取得できている、というだけです。

これにより、実際にテーブルには値が格納されているのに、なぜかfilterが効かず、値が取得できない、という事象が発生します。

必ず`nextToken`を使って次のデータがないかを確認するようにしましょう。