如何为DynamoDB餐厅支票表实现多条件Put校验?
我有一个名为Cheque的DynamoDB表,用于表示餐厅/酒吧的餐桌账单。希望对该表执行条件Put请求,仅当满足以下所有条件时才能创建新账单:
tableNumber当前不存在;restaurantId当前不存在;- 该餐桌的
isOpen状态为false(即未开单)。
我通过以下Terraform代码创建DynamoDB表:
resource "aws_dynamodb_table" "ChequesDDB" { name = "Cheques_${var.env_name}" hash_key = "id" billing_mode = "PROVISIONED" read_capacity = 5 write_capacity = 5 stream_enabled = true stream_view_type = "NEW_AND_OLD_IMAGES" attribute { name = "id" type = "S" } attribute { name = "tableNumber" type = "N" } global_secondary_index { name = "TableNumber" hash_key = "tableNumber" write_capacity = 5 read_capacity = 5 projection_type = "ALL" } }
注:不确定是否需要将tableNumber设置为二级索引,烦请告知是否必要。
随后我尝试用以下代码创建新账单:
const tableData: Cheque = { id: randomUUID(), isOpen: true, tableNumber: cheque.tableNumber, restaurantId: cheque.restaurantId, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; const params: DynamoDB.DocumentClient.PutItemInput = { TableName: env.CHEQUE_DDB, Item: tableData, ConditionExpression: "attribute_not_exists(tableNumber)" }; await this.db.put(params).promise();
起初仅尝试添加一个条件:确保tableNumber不存在,但每次执行代码都会创建新条目,导致同一tableNumber存在多个已开账单。
若将条件表达式改为attribute_not_exists(id),则能阻止同一id创建重复条目,这是否因为id是主键?
请问如何对非主键字段应用上述条件,避免同一桌号重复开单?
为什么attribute_not_exists(tableNumber)不生效?
DynamoDB的attribute_not_exists(attribute)是判断当前要写入的这条Item自身是否包含该属性,而非整个表中是否存在相同属性值的其他Item。所以你之前的条件只是检查新账单里有没有tableNumber字段,根本不会限制其他Item使用同一桌号,这才导致重复开单。
而attribute_not_exists(id)生效,确实是因为id是主键——DynamoDB强制保证主键唯一性,当你尝试写入相同主键的Item时,条件判断会直接失败,阻止重复创建。
实现餐桌号唯一开单的正确方式
要实现“同一餐厅内的同一桌号只能存在一个未关闭的账单”,需要从主键设计和条件表达式两方面调整:
修改为复合主键
把表的主键设置为分区键restaurantId+ 排序键tableNumber,这样天然就能保证同一餐厅内的桌号唯一。修改后的Terraform代码如下:resource "aws_dynamodb_table" "ChequesDDB" { name = "Cheques_${var.env_name}" billing_mode = "PROVISIONED" read_capacity = 5 write_capacity = 5 stream_enabled = true stream_view_type = "NEW_AND_OLD_IMAGES" # 复合主键:分区键+排序键确保餐厅内桌号唯一 hash_key = "restaurantId" range_key = "tableNumber" attribute { name = "restaurantId" type = "S" } attribute { name = "tableNumber" type = "N" } # 若需通过id查询账单,添加此全局二级索引 global_secondary_index { name = "IdIndex" hash_key = "id" write_capacity = 5 read_capacity = 5 projection_type = "ALL" } }调整条件表达式
写入新账单时,需要判断:要么该餐厅的这个桌号还没有任何账单,要么已有账单但处于关闭状态(isOpen=false)。对应的条件表达式如下:const params: DynamoDB.DocumentClient.PutItemInput = { TableName: env.CHEQUE_DDB, Item: tableData, ConditionExpression: "(attribute_not_exists(restaurantId) AND attribute_not_exists(tableNumber)) OR isOpen = :falseVal", ExpressionAttributeValues: { ":falseVal": false } };
关于二级索引的必要性
你之前创建的TableNumber全局二级索引完全没必要——修改主键后,tableNumber已经是排序键,可以直接通过restaurantId+tableNumber查询对应账单。如果业务需要通过id单独查询账单,才需要添加以id为分区键的全局二级索引(如上面代码所示)。
内容的提问来源于stack exchange,提问作者Stretch0

