51

Is there a way to achieve the following few points with updateItem:

  1. Add attributes if the attributes not exist in DynamoDB
  2. Update attributes if the attributes exist in DynamoDB
  3. Leave those attributes as what they are if the attributes are not contained in the params.

Here is an example: This is the object in DynamoDB:

{
    id: "1234",
    variable1: "hello",
    variable2: "world"
}

Here is the input that I wish to update:

{
    id: "1234",
    variable1: "hello2",
    variable23: "dog"  // the variable name "variable23" could be anything
}

Here is the updated item in the DynamoDB that I want to achieve:

{
    id: "1234",
    variable1: "hello2",
    variable2: "world",
    variable23: "dog"
}

The "variable23" could be any variable name as input.

I use node.js

Sebastian Nielsen
  • 3,835
  • 5
  • 27
  • 43
Pano
  • 2,099
  • 6
  • 16
  • 24

9 Answers9

83

This is exactly what AWS.DynamoDB.DocumentClient's update method does.

There is already a sample code on how to use the update method here for AWS SDK for JavaScript in Node.js.

For example:

'use strict';

const aws = require('aws-sdk');

// It is recommended that we instantiate AWS clients outside the scope of the handler 
// to take advantage of connection re-use.
const docClient = new aws.DynamoDB.DocumentClient();

exports.handler = (event, context, callback) => {
    const params = {
        TableName: "MYTABLE",
        Key: {
            "id": "1"
        },
        UpdateExpression: "set variable1 = :x, #MyVariable = :y",
        ExpressionAttributeNames: {
            "#MyVariable": "variable23"
        },
        ExpressionAttributeValues: {
            ":x": "hello2",
            ":y": "dog"
        }
    };

    docClient.update(params, function(err, data) {
        if (err) console.log(err);
        else console.log(data);
    });
};
Khalid T.
  • 10,039
  • 5
  • 45
  • 53
  • 5
    Can you show me some code on my example? I've seen that example and still confused. If I have 30 attributes then do I need to write expressions for 30 attributes? what if I have new attributes? Thanks! – Pano Jan 28 '17 at 22:37
  • 1
    You need to specify the attributes you only wish to update. So, if you need to update 30 attributes, then you'll have to write the update expression for all 30 attributes. See my updated answer for the code sample. – Khalid T. Jan 28 '17 at 22:53
  • What if variable23 becomes variable67?, how should I handle that part? I don't even know what the input is, how to make the update? – Pano Jan 28 '17 at 22:56
  • You can use `ExpressionAttributeNames` for that and pass the name as an input to the function. See the updated code sample. – Khalid T. Jan 28 '17 at 23:01
  • 2
    so I should loop through the input object, add the attribute names to "ExpressionAttributeNames", add corresponding values to "ExpressionAttributeValues", then generate a expression string to put inside UpdateExpression? – Pano Jan 28 '17 at 23:05
  • 3
    If I were you and I am supposed to store unpredictable data as in your case in DynamoDB, I'd put it *as it is* as a single JSON object using one fixed attribute name and let the back-end application parse the JSON object upon retrieval. – Khalid T. Jan 28 '17 at 23:18
  • 6
    Seems inelegant to me. For a simple case can't we just pass an object and have the parameters update in a similar way to the putItem method?! For other casesUpdateExpression, ExpressionAttributes, etc will be relevant – darkace Jun 25 '20 at 12:42
30

I think some of the examples are a bit confusing. If I have the following table columns

ID  | Name | Age

And I want to update the Name attribute and leave the Age attribute unchanged.

const updateName = async () => {
  const aws = require('aws-sdk');
  const docClient = new aws.DynamoDB.DocumentClient();

  const newName = 'Bob';

  const params = {
    TableName: 'myTable',
    Key: {
      ID: 'myId',
    },
    UpdateExpression: 'set Name = :r',
    ExpressionAttributeValues: {
      ':r': newName,
    },
  };

  await docClient.update(params).promise();
}

updateName();

This seemed a bit more simple.

thedanotto
  • 6,895
  • 5
  • 45
  • 43
  • 3
    This example made me finally understand the syntax. Thank you. – MondQ Feb 06 '21 at 09:40
  • Thanks , easy to understand – kubs Jun 25 '21 at 11:04
  • 1
    upvoted for the name, and a good answer :) – Daniel Otto Apr 29 '22 at 07:16
  • This is a great example. The syntax is made overly complicated by the use of `ExpressionAttributeNames` instead of just using the table properties in the format string directly. Simply using `ExpressionAttributeValues` suffices in the majority of cases. – h0r53 Nov 01 '22 at 14:47
26

You can update attributes dynamically. see below code.

export const update = (item) => {
  console.log(item)
  const Item = {
    note: "dynamic",
    totalChild: "totalChild",
    totalGuests: "totalGuests"
  };
  let updateExpression='set';
  let ExpressionAttributeNames={};
  let ExpressionAttributeValues = {};
  for (const property in Item) {
    updateExpression += ` #${property} = :${property} ,`;
    ExpressionAttributeNames['#'+property] = property ;
    ExpressionAttributeValues[':'+property]=Item[property];
  }

  
  console.log(ExpressionAttributeNames);


  updateExpression= updateExpression.slice(0, -1);
  
  
   const params = {
     TableName: TABLE_NAME,
     Key: {
      booking_attempt_id: item.booking_attempt_id,
     },
     UpdateExpression: updateExpression,
     ExpressionAttributeNames: ExpressionAttributeNames,
     ExpressionAttributeValues: ExpressionAttributeValues
   };

   return dynamo.update(params).promise().then(result => {
       return result;
   })
   
}
Cemil Birinci
  • 401
  • 4
  • 11
17

Here is a more secure and up-to-date function to achieve this:

const {
  DynamoDBClient, UpdateItemCommand,
} = require('@aws-sdk/client-dynamodb');
const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb');

const client = new DynamoDBClient({});

/**
 * Update item in DynamoDB table
 * @param {string} tableName // Name of the target table
 * @param {object} key // Object containing target item key(s)
 * @param {object} item // Object containing updates for target item
 */
const update = async (tableName, key, item) => {
  const itemKeys = Object.keys(item);

  // When we do updates we need to tell DynamoDB what fields we want updated.
  // If that's not annoying enough, we also need to be careful as some field names
  // are reserved - so DynamoDB won't like them in the UpdateExpressions list.
  // To avoid passing reserved words we prefix each field with "#field" and provide the correct
  // field mapping in ExpressionAttributeNames. The same has to be done with the actual
  // value as well. They are prefixed with ":value" and mapped in ExpressionAttributeValues
  // along witht heir actual value
  const { Attributes } = await client.send(new UpdateItemCommand({
    TableName: tableName,
    Key: marshall(key),
    ReturnValues: 'ALL_NEW',
    UpdateExpression: `SET ${itemKeys.map((k, index) => `#field${index} = :value${index}`).join(', ')}`,
    ExpressionAttributeNames: itemKeys.reduce((accumulator, k, index) => ({ ...accumulator, [`#field${index}`]: k }), {}),
    ExpressionAttributeValues: marshall(itemKeys.reduce((accumulator, k, index) => ({ ...accumulator, [`:value${index}`]: item[k] }), {})),
  }));

  return unmarshall(Attributes);
};
Arno
  • 461
  • 4
  • 13
7

Here is a utility method to do that:

update: async (tableName, item, idAttributeName) => {

    var params = {
        TableName: tableName,
        Key: {},
        ExpressionAttributeValues: {},
        ExpressionAttributeNames: {},
        UpdateExpression: "",
        ReturnValues: "UPDATED_NEW"
    };

    params["Key"][idAttributeName] = item[idAttributeName];

    let prefix = "set ";
    let attributes = Object.keys(item);
    for (let i=0; i<attributes.length; i++) {
        let attribute = attributes[i];
        if (attribute != idAttributeName) {
            params["UpdateExpression"] += prefix + "#" + attribute + " = :" + attribute;
            params["ExpressionAttributeValues"][":" + attribute] = item[attribute];
            params["ExpressionAttributeNames"]["#" + attribute] = attribute;
            prefix = ", ";
        }
    }

    return await documentClient.update(params).promise();
}
Daniel Barral
  • 3,896
  • 2
  • 35
  • 47
5

Here's the batch update function I use, with an emphasis on readability.

const documentClient = new AWS.DynamoDB.DocumentClient(options);

const update = async ({  tableName,  primaryKeyName,  primaryKeyValue,  updates }) => {
    const keys = Object.keys(updates)
    const keyNameExpressions = keys.map(name => `#${name}`)
    const keyValueExpressions = keys.map(value => `:${value}`)
    const UpdateExpression = "set " + keyNameExpressions
        .map((nameExpr, idx) => `${nameExpr} = ${keyValueExpressions[idx]}`)
        .join("; "),
    const ExpressionAttributeNames = keyNameExpressions
        .reduce((exprs, nameExpr, idx) => ({ ...exprs, [nameExpr]: keys[idx] }), {})
    const ExpressionAttributeValues = keyValueExpressions
        .reduce((exprs, valueExpr, idx) => ({ ...exprs, [valueExpr]: updates[keys[idx]] }), {})

    const params = {
        TableName: tableName,
        Key: { [primaryKeyName]: primaryKeyValue },
        UpdateExpression,
        ExpressionAttributeNames,
        ExpressionAttributeValues
    };
    return documentClient.update(params).promise();
}

// USAGE
let { ID, ...fields} = {
    ID: "1234",
    field1: "hello",
    field2: "world"
}

update('tableName', 'ID', ID, fields) 
Spankied
  • 1,626
  • 13
  • 21
  • There is comma instead of semicolon at line 9 (I can't edit it, to small change), but everything works like a charm :D – jonzee Nov 14 '21 at 19:14
3

I made this using dynamo DB client:

updateItem(item: { [key: string]: any }) {
  const marshaledItem = marshall(item, { removeUndefinedValues: true, });
  const marshaledItemKeys = Object.entries(marshaledItem);

  const params: UpdateItemInput = {
    TableName: this.tableName,
    UpdateExpression: 'set',
    ExpressionAttributeNames: {},
    ExpressionAttributeValues: {},
    Key: marshall({ pk: item.pk, sk: item.sk })
  };

  marshaledItemKeys.forEach(([key, value] ) => {
    if (key === 'sk' || key === 'pk') return;
    params.UpdateExpression += ` #${key} = :${key},`;
    params.ExpressionAttributeNames[`#${key}`] = key;
    params.ExpressionAttributeValues[`:${key}`] = value;
  })

  params.UpdateExpression = params.UpdateExpression.slice(0, -1);
  console.log('REVEAL YOURSELF, YOU MIGHTY BUG: ', params);

  return this.dynamoDbClient.send(new UpdateItemCommand(params));
}

This worked really well for me. Marshall and unmarshall are part of:

import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';

If I pass values that are undefined it will remove those values from the query. If I keep them null it will overwrite them with null

Here is an example how I use it:

async updatePatient(data: PutPatientData): Promise<DBPatient> {
    const {
      pk,
      sk,
      databaseId,
      betterPatientId,
      clinicientPatientId,
      latestClinicientCaseId,
      firstName,
      lastName,
      email,
      birthday,
      gender,
      phone,
    } = data;

    if (!pk && !databaseId) throw Error('Please provide PK or databaseId');
    if (!sk && !betterPatientId) throw Error('Please provide SK or betterPatientId');

    const patientRequestData = {
      pk: pk || `DATABASE#${databaseId}`,
      sk: sk || `PATIENT#${betterPatientId}`,
      itemType: 'Patient',
      lastUpdatedAt: DateTime.now().toString(),
      latestClinicientCaseId: latestClinicientCaseId || undefined,
      clinicientPatientId: clinicientPatientId || undefined,
      firstName: firstName || undefined,
      lastName: lastName || undefined,
      email: email || undefined,
      birthday: birthday || undefined,
      gender: gender || undefined,
      phone: phone || undefined,
      betterPatientId: betterPatientId || undefined,
    } as DBPatient;
    // Composite key
    if (email && birthday) patientRequestData['itemId'] = `PATIENT#${email}#${birthday}`;
        console.log('PATIENT UPDATE', patientRequestData)
    return this.updateItem(patientRequestData).then(() => patientRequestData);
}
Bogdan Polovko
  • 115
  • 1
  • 11
2

For anyone here for the velocity template version of the solution, they have something documented in their docs, which it took me a while to find, so here's a link if it helps anyone else

https://docs.aws.amazon.com/appsync/latest/devguide/resolver-mapping-template-reference-dynamodb.html#id4 under 'option 2'

webjay
  • 5,358
  • 9
  • 45
  • 62
andy mccullough
  • 9,070
  • 6
  • 32
  • 55
0

Sorry for being late to the party, but this was a top [non-aws-documentation] result on google and didn't answer my use case - using DocumentClient without [un]marshalling AND having a dynamic item. So I want to drop my 2 cents here and try to be helpful by merging the approved answer from @khalid-t and one from @Arno.

'use strict';
const aws = require('aws-sdk');
const docClient = new aws.DynamoDB.DocumentClient();
const updateItem = async (pk, item) => await docClient.update({
    TableName,
    Key: {pk},
    UpdateExpression: 'set ' + Object.keys(item).map(k => `#${k} = :${k}`).join(', '),
    ExpressionAttributeNames: Object.entries(item).reduce((acc, cur) => ({...acc, [`#${cur[0]}`]: cur[0]}), {}),
    ExpressionAttributeValues: Object.entries(item).reduce((acc, cur) => ({...acc, [`:${cur[0]}`]: cur[1]}), {}),
}).promise();
Edgar Zagórski
  • 1,509
  • 11
  • 14