【问题标题】:AWS Dynamodb document client UpdateCommandAWS Dynamodb 文档客户端 UpdateCommand
【发布时间】:2021-12-07 17:15:57
【问题描述】:

AWS DynamoDB 文档中的 UpdateCommand 没有很好的文档记录且难以使用。

我想使用 update 命令来获取一个 Javascript 对象并将其中的所有键插入到与主键匹配的对象中。

看着InsertCommand我以为会是这样的简单:

async updateItem(tableName: string, primaryKeyName: string, id: string, item: { [key: string]: any }) {
    const input: UpdateCommandInput = {
      Key: {
        [primaryKeyName]: id,
      },
      AttributeUpdates: item,
      TableName: tableName,
    };

    const command = new UpdateCommand(input);

    return await this.client.send(command);
  }

但这似乎失败了。

【问题讨论】:

    标签: node.js typescript amazon-web-services amazon-dynamodb


    【解决方案1】:

    现在,当我环顾四周时,很难找到文档,但我设法通过几个不同的来源将其拼凑在一起。

    我创建了一个函数,它将获取一个对象,然后用主键引用的对象上的所有这些属性进行覆盖。我希望这可以帮助那些难以弄清楚UpdateCommand 是如何工作的人。

    import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
    import {
      UpdateCommandInput,
      UpdateCommand,
      DynamoDBDocumentClient,
    } from '@aws-sdk/lib-dynamodb';
    
    export class DynamoDBService {
      private client: DynamoDBDocumentClient;
      constructor() {
        const client = new DynamoDBClient({
          region: config.get(<<your_region>>),
        });
        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: true, // false, by default.
        };
        this.client = DynamoDBDocumentClient.from(client, { marshallOptions });
      }
    
      /**
       * Takes a javascript object and transforms it into update expressions on the dynamodb object.
       *
       * It translates all of the actions to SET which will overwrite any attribute that is already there.
       * It works best with simple types but can also serialise arrays and objects.
       *
       * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/dynamodb-example-dynamodb-utilities.html
       *
       * @param tableName Name of the table to update on
       * @param primaryKeyName The primary key name to update on
       * @param id The primary key value
       * @param item The item to update
       */
      async upsertProperties(tableName: string, primaryKeyName: string, id: string, item: { [key: string]: any }) {
        const updatedItem = this.removePrimaryKey(primaryKeyName, item);
        const { updateExpression, expressionAttribute, expressionAttributeNames } =
          this.createUpdateExpressions(updatedItem);
        const input: UpdateCommandInput = {
          Key: {
            [primaryKeyName]: id,
          },
          UpdateExpression: `SET ${updateExpression.join(', ')}`,
          ExpressionAttributeValues: expressionAttribute,
          ExpressionAttributeNames: expressionAttributeNames,
          TableName: tableName,
        };
    
        const command = new UpdateCommand(input);
        return await this.client.send(command);
      }
    
      /**
       * We alias properties to be sure we can insert reserved names (status fx).
       *
       * It is a bit complicated:
       * updateExpression: The actual update state, example: SET #alias = :placeholder
       * expressionAttribute: The value to insert in placeholder example: :placeholder = value
       * expressionAttributeNames: Are the aliases properties to avoid clashe: #alias = key
       *
       * So in the end it ties together and both the inserted value and alias are fixed.
       */
      private createUpdateExpressions(item: { [key: string]: any }) {
        const updateExpression: string[] = [];
        const expressionAttribute: { [key: string]: any } = {};
        const expressionAttributeNames: { [key: string]: any } = {};
        Object.keys(item).map((key) => {
          const placeholder = `:p${key}`;
          const alias = `#a${key}`;
          updateExpression.push(`${alias} = ${placeholder}`);
          expressionAttribute[placeholder] = item[key];
          expressionAttributeNames[alias] = key;
        });
        return { updateExpression, expressionAttribute, expressionAttributeNames };
      }
    
      /**
       * Remove primary key from the object or else we cannot make an
       * update statement because the primary key would be overwritten
       * which violates the insert.
       *
       * Copy to new object to ensure we don't manipulate the reference.
       */
      private removePrimaryKey(primaryKeyName: string, item: { [key: string]: any }) {
        const itemWithoutPrimaryKey = { ...item };
        delete itemWithoutPrimaryKey[primaryKeyName];
        return itemWithoutPrimaryKey;
      }
    }
    
    

    【讨论】:

      【解决方案2】:

      我必须在 @kristian-barrett 的出色答案的基础上处理嵌套 dynamodb 条目的部分更新。

      问题是像{foo: {bar: "updated value" }} 这样的传入更新会覆盖所有foo 属性,而不仅仅是foo.bar 一个。

      在 ddb 中的前:

      {
        "foo": {
          "bar": "old value",
          "baz": "other value"
        }
      }
      

      会这样更新:

      {
        "foo": {
          "bar": "updated value" // no more foo.baz
        }
      }
      

      在解析传入的部分更新时,基本上需要的是 dynamodb 表达式的递归构建,所以

      这是代码。

      import { DynamoDBClient } from "@aws-sdk/client-dynamodb"
      import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"
      
      /** The AWS DynamoDB service client object. */
      export const ddbClient = new DynamoDBClient({})
      
      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: true, // 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 }
      
      /** The DynamoDB Document client. */
      export const ddbDocClient = DynamoDBDocumentClient.from(ddbClient, translateConfig)
      
      // eslint-disable-next-line no-secrets/no-secrets
      // Code adapted from : https://stackoverflow.com/questions/68358472/aws-dynamodb-document-client-updatecommand
      
      export interface CreateDDBUpdateExpressionProps {
        /**
         * Item representing the partial update on an object
         */
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        item: any
      
        /**
         * Root describing the context of the item, relative to the top-parent update object
         */
        root: {
          /**
           * Path is the concatenation of attributes leading to item
           * It is used for placeholder and local alias in expressions
           *
           * ex: { foo: { bar: { the item }}} will have as path: foobar
           * Will appear in expression: SET #foo.#foobar = :foobar
           */
          path: string
      
          /**
           * Alias is the cumulative concatenation of paths leading to item
           * It is used for building the alias from top-parent update object
           *
           * ex: { foo: { bar: { the item }}} will have as:
           * - local alias: #foobar
           * - parent alias to the item: #foo
           * - full alias as parent + local = #foo.#foobar
           */
          alias: string
        }
      }
      
      /**
       * updateExpression: The actual update state, example: SET #alias = :placeholder
       * expressionAttribute: The value to insert in placeholder example: :placeholder = value
       * expressionAttributeNames: Are the aliases properties to avoid clashes: #alias = key
       */
      export interface DDBUpdateExpression {
        updateExpression: string
        expressionAttributeValues: { [key: string]: unknown }
        expressionAttributeNames: { [key: string]: string }
      }
      
      /**
       * Recursive translation of a update object to an unfinished DynamoDB expression
       *
       * @param param0 a CreateDDBUpdateExpressionProps
       * @returns an unfinished DDBUpdateExpression (lacking 'SET' in expression)
       */
      function createDDBUpdateExpression({ item, root }: CreateDDBUpdateExpressionProps): DDBUpdateExpression {
        const rootPath = root.path ? `${root.path}` : "" // rootPath to be added for all keys of item
        const rootAlias = root.alias ? `${root.alias}.` : ""
        const filteredItem = { ...item } // unsure if still usefull besides removing non-enumerable properties
      
        const updateExpressionArr: string[] = []
        const expressionAttributeValues: { [key: string]: unknown } = {}
        const expressionAttributeNames: { [key: string]: string } = {}
      
        Object.keys(filteredItem).forEach((key) => {
          // build the full path to attribute being inspected
          const fullKey = `${rootPath}${key}`
      
          if (typeof filteredItem[key] === "object") {
            // need to recurse on each key as it can be an object representing a partial update.
            // https://stackoverflow.com/questions/51911927/update-nested-map-dynamodb
      
            const {
              updateExpression: nestedExpression,
              expressionAttributeValues: nestedValues,
              expressionAttributeNames: nestedNames,
            } = createDDBUpdateExpression({
              item: filteredItem[key],
              root: {
                path: `${rootPath}${key}`,
                alias: `${rootAlias}#${rootPath}${key}`,
              },
            })
      
            updateExpressionArr.push(nestedExpression)
            Object.assign(expressionAttributeValues, nestedValues)
            Object.assign(expressionAttributeNames, nestedNames)
            expressionAttributeNames[`#${fullKey}`] = key
            return
          }
      
          if (typeof filteredItem[key] === "function") {
            // bail out, methods should not be there anyway nor appear in update expression
            return
          }
      
          // leaf case where key points to a simple primitive type (ie no object nor function)
          const placeholder = `:${fullKey}`
          const alias = `#${fullKey}`
          updateExpressionArr.push(`${rootAlias}${alias} = ${placeholder}`)
          expressionAttributeValues[placeholder] = item[key]
          expressionAttributeNames[alias] = key
        })
      
        const updateExpression = updateExpressionArr.join(", ")
      
        return { updateExpression, expressionAttributeValues, expressionAttributeNames }
      }
      
      /**
       * We alias properties to be sure we can insert reserved names.
       *
       * @param item a js object representing a partial update
       * @param primaryKeyName the name of property considered as primary key (to remove from update expression)
       * @returns necessary expression properties to update a DynamoDB item
       */
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      export function createDDBUpdateExpressions(item: any, primaryKeyName?: string): DDBUpdateExpression {
        const filteredItem = { ...item }
        if (primaryKeyName) {
          delete filteredItem[primaryKeyName] // remove primary key to forbid id update
        }
      
        const { updateExpression, expressionAttributeValues, expressionAttributeNames } = createDDBUpdateExpression({
          item: filteredItem,
          root: { path: "", alias: "" },
        })
      
        const updateExpressionSet = `SET ${updateExpression}`
      
        return { updateExpression: updateExpressionSet, expressionAttributeValues, expressionAttributeNames }
      }
      ```
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2020-10-07
        • 2012-04-29
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-01-15
        • 2021-01-17
        • 1970-01-01
        相关资源
        最近更新 更多