10
4

DynamoDB の Get/Put/Query/Scan/Update Command のサンプル

Last updated at Posted at 2022-12-12

背景

DynamoDB を使う為の、よく使うコマンドの操作例と、起きたエラーの対処などの記録

  • GetCommand
  • PutCommand
  • QueryCommand
  • ScanCommand
  • UpdateCommand

利用例

前提

AWS SDK V3 に記載のある Client ファイルを使う

以下 2ファイルを利用

プロキシを利用する場合は、DynamoDBClient のところで設定しておく

ddbClient.ts
// Create the DynamoDB service client module using ES6 syntax.
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { NodeHttpHandler } from '@aws-sdk/node-http-handler';
// import { HttpsProxyAgent } from 'hpagent';
// const httpsAgent = new HttpsProxyAgent({ proxy: 'http://10.20.30.40:8080' });

// Set the AWS Region.
export const REGION = "ap-northeast-1";
// Create an Amazon DynamoDB service client object.
export const ddbClient = new DynamoDBClient({ 
    region: REGION,
    // requestHandler: new NodeHttpHandler({
    //     httpAgent: httpsAgent,
    //     httpsAgent: httpsAgent
    // })
});
ddbDocClient.ts
// Create a service client module using ES6 syntax.
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { ddbClient } from "./ddbClient";
const marshallOptions = {
  // Whether to automatically convert empty strings, blobs, and sets to `null`.
  convertEmptyValues: false, // false, by default.
  // Whether to remove undefined values while marshalling.
  removeUndefinedValues: false, // false, by default.
  // Whether to convert typeof object to map attribute.
  convertClassInstanceToMap: false, // false, by default.
};
const unmarshallOptions = {
  // Whether to return numbers as a string instead of converting them to native JavaScript numbers.
  wrapNumbers: false, // false, by default.
};
const translateConfig = { marshallOptions, unmarshallOptions };
// Create the DynamoDB document client.
const ddbDocClient = DynamoDBDocumentClient.from(ddbClient, translateConfig);
export { ddbDocClient };

テーブル

こんな感じのテーブルに対して
image.png

image.png

Tips

Sort: 昇順・降順

以下のように ScanIndexForward 使えばOK。

sort
  const queryCommandInput = {
    Limit: 1, // 取得数。like "TOP"
    ScanIndexForward: false, // false: DESC, true: ASC 
  };

属性が格納されてるか?の確認方法

以下注意

  • attribute_exists()
    • id 入ってるか?で見ると、消した場合に、(empty) として、属性は存在しているので反応してしまう
    • Artist/Enterpreneur みたいに、複数の Table を単一Tableで保存する場合に、特定の属性があるか?を見るために使うのかな
  • size()
    • 対象属性の内容が一定文字数以上、とかで調べられるので、こちらが良さそう

stream/db

  • SDK.libClient で取得する場合は、低層APIを wrap してるので、気にしなくてOK
  • Stream から取得した場合、以下のように型意識が必要
    Number(dynamodb.Keys.ID.N ?? 0)
    Boolean(dynamodb.Keys.IsVaild.BOOL)
    • 途中で取得すると、string

PutCommand

Sort Key の属性名の "#" を入れてるけど、以下な感じで文字列化してやれば問題なく行けた。
同一キーに対しては上書きなので、特定の属性だけ上書きしたい場合には、UpdateCommand を

PutCommand
const putRecord = async (date: Date, postID: number) => {
    const timestamp = date.getTime();
    const params = {
        TableName: "TestCompositeKey",
        Item: {
            uid: "user02",
            "timestamp#post_id": `${timestamp}#${postID}`,
            timestamp: timestamp,
            timestampString: date.toLocaleString(),
            post_id: postID,
        },

    };
    const result = await ddbDocClient.send(new PutCommand(params));
}

取得する際に困ったときは以下

UpdateCommand

Put だと Key が同じ場合に完全に置換されてしまうので、
一部属性値の置換や追加が行いたい時はこちらを使う

Update Example
const updateItem = async (content: any) => {
    const baseParams = {
        TableName: "TestCompositeKey",
        Key: {
            uid: content.PK,
            "timestamp#post_id": content.SK,
        },
        ExpressionAttributeNames: {
            "#data": "data",
        },
        ExpressionAttributeValues: {
            ":data": content.data,
        },
        UpdateExpression: `SET #data = :data ${(content.data2 ? ", #data2=:data2" : "")}`,
    };
    const mergeData2 = {
        ExpressionAttributeNames: {
            "#data2": "data2",
        },
        ExpressionAttributeValues: {
            ":data2": content.data2
        },
    };

    const params = (content.data2) ?
        _.merge(baseParams, mergeData2) :
        baseParams;
    return await ddbDocClient.send(new UpdateCommand(params));
};

上の例の詳細は以下で

これだけだと、動的といっても想定のプロパティによって切り替えるだけなので、以下で動的プロパティの場合を追記

動的に渡された項目だけの更新式を生成させる

GetCommand

PutCommand と大して変わらないので、特に問題無し

GetCommand
const getRecord = async (PK: string, compositeSK: string) => {
    const params = {
        TableName: "TestCompositeKey",
        Key: {
            uid: PK,
            "timestamp#post_id": compositeSK,
        },
    }
    return (await ddbDocClient.send(new GetCommand(params))).Item;
}

ScanCommand

基本全部取るのが Scan だけど、今回は Filter をかけて取得した例

以下、注意

フィルタ式は、Scan の完了後、結果が返される前に適用されます。そのため、Scan は、フィルタ式があるかどうかにかかわらず、同じ量の読み込みキャパシティーを消費します。

ScanCommand
const scanRecords = async (startDate: Date, endDate: Date) => {
    const params = {
        TableName: "TestCompositeKey",
        ExpressionAttributeNames: {
            "#timestamp": "timestamp"
        },
        ExpressionAttributeValues: {
            // ":PK": userID,
            ":sortkeyval1": startDate.getTime(),
            ":sortkeyval2": endDate.getTime()
        },
        FilterExpression: "#timestamp BETWEEN :sortkeyval1 AND :sortkeyval2"
    }
    const usersGotten = (await ddbDocClient.send(new ScanCommand(params))).Items ?? [];
    return usersGotten;
}

属性が格納されてるか?で見落としがち

  • attribute_exists()
    • id 入ってるか?で見ると、消した場合に、(empty) として、属性は存在しているので反応してしまう
    • Artist/Enterpreneur みたいに、複数の Table を単一Tableで保存する場合に、特定の属性があるか?を見るために使うのかな
  • size()
    • 対象属性の内容が一定文字数以上、とかで調べられるので、こちらが良さそう
      size( #token ) >= :tokenLength"

QueryCommand

インデックスを設定した場合は、"IndexName" を利用する

QueryCommand
const queryRecords = async (userID: string, startDate: Date, endDate: Date) => {
    const paramsQuery = {
        TableName: "TestCompositeKey",
    //  IndexName: "index_post_id",
        ExpressionAttributeNames: {
            "#PK": "uid",
            "#SK": "timestamp#post_id"
        },
        ExpressionAttributeValues: {
            ":PK": userID,
            ":sortkeyval1": startDate.getTime(),
            ":sortkeyval2": endDate.getTime()
        },
        KeyConditionExpression: "#PK = :PK AND ( #SK  BETWEEN :sortkeyval1 AND :sortkeyval2 )"
    };
    console.log("query params:", inspect(paramsQuery));
    const queriedRecords = (await ddbDocClient.send(new QueryCommand(paramsQuery))).Items ?? [];
    return queriedRecords;
}

使える演算子と、関数はこちら

error 対処

ValidationException: Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword:

timestamp のような予約語使う場合は、プレースホルダーを使えってやつ

"timestamp" が予約語なので、そのまま FilterExpression では使えない
    const params = {
        TableName: "TestCompositeKey",
        ExpressionAttributeValues: {
            // ":PK": userID,
            ":sortkeyval1": startDate.getTime(),
            ":sortkeyval2": endDate.getTime()
        },
        FilterExpression: "timestamp BETWEEN :sortkeyval1 AND :sortkeyval2"
    }
対策後。ExpressionAttributeNames で定義して使う
    const params = {
        TableName: "TestCompositeKey",
        ExpressionAttributeNames: {
            "#timestamp": "timestamp"
        },
        ExpressionAttributeValues: {
            // ":PK": userID,
            ":sortkeyval1": startDate.getTime(),
            ":sortkeyval2": endDate.getTime()
        },
        FilterExpression: "#timestamp BETWEEN :sortkeyval1 AND :sortkeyval2"
    }
    const usersGotten = (await ddbDocClient.send(new

ValidationException: Query condition missed key schema element: uid

キーが無いよ。
そう、Key と KeyConditionExpression は同時には使えない

エラー
  const paramsQuery = {
    TableName: "TestCompositeKey",
    Key: {  // これが効かない
      uid: userID
    },
    ExpressionAttributeNames: {
      "#SK": "timestamp#post_id"
    },
    ExpressionAttributeValues: {
      ":sortkeyval1": startDate.getTime(),
      ":sortkeyval2": endDate.getTime()
    },
    KeyConditionExpression: "#SK  BETWEEN :sortkeyval1 AND :sortkeyval2 "
  };
対策後: KeyConditionExpression でまとめればOK
  const paramsQuery = {
    TableName: "TestCompositeKey",
    ExpressionAttributeNames: {
      "#PK": "uid",
      "#SK": "timestamp#post_id"
    },
    ExpressionAttributeValues: {
      ":PK": userID,
      ":sortkeyval1": startDate.getTime(),
      ":sortkeyval2": endDate.getTime()
    },
    KeyConditionExpression: "#PK = :PK AND ( #SK  BETWEEN :sortkeyval1 AND :sortkeyval2 )"
  };

ValidationException: One or more parameter values were invalid: Condition parameter type does not match schema type

Schema と違う型使ってるよって話

エラー: timestamp は Epoch Number だけど、Sort Key にした際に、文字列になってる為
  const paramsQuery = {
    TableName: "TestCompositeKey",
    ExpressionAttributeNames: {
      "#PK": "uid",
      "#SK": "timestamp#post_id"
    },
    ExpressionAttributeValues: {
      ":PK": userID,
      ":sortkeyval1": startDate.getTime(),
      ":sortkeyval2": endDate.getTime()
    },
    KeyConditionExpression: "#PK = :PK AND ( #SK  BETWEEN :sortkeyval1 AND :sortkeyval2 )"
  };
対策後:文字列化するのみ
  const paramsQuery = {
    TableName: "TestCompositeKey",
    ExpressionAttributeNames: {
      "#PK": "uid",
      "#SK": "timestamp#post_id"
    },
    ExpressionAttributeValues: {
      ":PK": userID,
      ":sortkeyval1": `${startDate.getTime()}`,
      ":sortkeyval2": `${endDate.getTime()}`
    },
    KeyConditionExpression: "#PK = :PK AND ( #SK  BETWEEN :sortkeyval1 AND :sortkeyval2 )"
  };

ValidationException: Value provided in ExpressionAttributeNames unused in expressions: keys: {#SK}

単に、未使用の ExpressionAttributeNames を残しておいちゃダメなので、消せってだけ

ValidationException: Value provided in ExpressionAttributeValues unused in expressions: keys: {:sortkeyval2, :sortkeyval1}

同様に、ExpressionAttributeValues も使わないのは消しましょう

ValidationException: Filter Expression can only contain non-primary key attributes: Primary key attribute:

PK/SK を filterExpression に定義したらダメです

ValidationException: KeyConditionExpressions must only contain one condition per key

PK/SK を、二回使ってはダメ
以下の場合、SK を二回使っているので error。対処法は BETWEEN を使えってこと

エラー例
KeyConditionExpression: "#PK =:PK AND ( :sortkeyval1 <= #SK AND #SK < :sortkeyval2 )"

VS Code

エラーメッセージなどは、CTRL + Click で該当コードへ

CTRL + Click で、Source の該当箇所へ
※Enter が必要な場合も有り

image.png

template.yaml

lambda console でデバッグする際は、Minify/Sourcemap を false にする

  • Minify
    • コード見やすくしないと
  • Sourcemap
    • Lambda 上の js コードを修正しながら行うのであれば、false のが分かりやすい
    Metadata: # Manage esbuild properties
      BuildMethod: esbuild
      BuildProperties:
        Minify: false
        Target: "es2020"
        Sourcemap: false

あとがき

PartiQL や SQL との比較もしてみたいなぁ

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4