进阶阅读约 2 分钟

DynamoDB 中的表达式属性名与值

DynamoDB 表达式是模板:你写占位符,然后在两个旁路映射里提供真实的属性名和值。#name 是一个名称 占位符;:value 是一个值占位符。把这两个搞混,DynamoDB 就拒绝整个调用。

DynamoDB 中 #name 和 :value 有什么区别?

#name 是属性名的占位符,通过 ExpressionAttributeNames 提供;:value 是属性值的占位符,通过 ExpressionAttributeValues 提供。用 #name 来避开保留字、点或空格,用 :value 来表示每一个字面量——DynamoDB 从不把值内联进表达式。它们不可互换;把两者搞混会抛出 ValidationException

  • #name 通过 ExpressionAttributeNames 替换一个属性名——每当一个属性与保留字冲突,或含有 点/空格时就用它。
  • :value 通过 ExpressionAttributeValues 替换一个值——DynamoDB 从不把字面量内联进表达式文本, 所以每个值都是一个占位符。
  • 它们不可互换。# 放到该放 : 的地方是一个 ValidationException,而不是一个安静的空操作。

从 SQL 过来,你两者都内联——WHERE status = 'published'。DynamoDB 两者都不内联。那个分离正是绊倒每个 新手的东西。

为什么有这两个映射

在 SQL 里,查询字符串承载一切:列名、字面量、运算符。DynamoDB 刻意把表达式的形状与它的数据分开。

值进它们自己的映射,于是 DynamoDB 能给每个值定类型(SNBOOL、…),而且解析器永远不必猜一个 字符串在哪里结束——没有引号或转义可弄错。参见 DynamoDB 中的数据类型 了解完整的类型标签列表。

名称得到同样的待遇,但出于一个不同的原因:DynamoDB 有一长串保留字,任何匹配其中之一的属性都不能 作为一个裸名出现在表达式里。占位符完全避开了这个保留。

保留字的坑

这是一张 CMS 文章表——分区键 BLOG#<blog>,排序键 ARTICLE#<slug>——它的属性读起来很自然,却碰巧与 保留字冲突:

属性保留?它持有什么
statusdraft / published
name作者显示名
size渲染后的字节长度
ttl归档过期(纪元)
slugURL slug

statusnamesizettl 全在 AWS 的保留字列表上,所以这个过滤在第一个词就失败:

FilterExpression  status = :s

DynamoDB 返回一个 ValidationException——"Attribute name is a reserved keyword; reserved keyword: status"。修法是一个名称占位符,绝不是重命名属性:

FilterExpression           #status = :s
ExpressionAttributeNames   { "#status": "status" }
ExpressionAttributeValues  { ":s": { "S": "published" } }

那个坑:slug 保留,所以一个你针对 slug 测试过的查询能用,于是你假设下一个也行。然后 status 破坏了它。完整列表在变动,所以别去背它——给每个名称都加占位符,你就永远不会被咬。

永远映射每个值

值是不容商量的:没有内联字面量的语法。哪怕一个普通数字也得一个占位符。这次更新把一篇文章标记为已发布、 盖上它的大小,并设一个 30 天的归档 TTL:

UpdateExpression:          SET #status = :s, #size = :sz, #ttl = :exp
ExpressionAttributeNames:  { "#status": "status", "#size": "size", "#ttl": "ttl" }
ExpressionAttributeValues: {
  ":s":   { "S": "published" },
  ":sz":  { "N": "20480" },
  ":exp": { "N": "1719792000" }
}

注意 :sz:exp 作为 N 字符串发送——DynamoDB 的数字类型在线缆上被编码为字符串。值映射也是你跨子句 复用一个值的地方:定义 :s 一次,在一个 ConditionExpression 和一个 FilterExpression 里都引用它。

手工构建这两个映射正是打字错误藏身之处。Expression Builder 把表达式字符串和两个映射一起生成,类型标签都已填好,所以占位符无法彼此失同步。

用名称处理嵌套和别扭的路径

# 占位符的作用不止于避开保留字。文档路径语法用点和方括号,所以一个字面包含一个点的属性——比如一个 元数据键 og.title——没有占位符就无法寻址:

ProjectionExpression       #og
ExpressionAttributeNames   { "#og": "og.title" }

没有它,DynamoDB 把 og.title 读作"og 映射里的 title 字段"——一个完全不同的东西。带空格或带前导 数字的名称是同样的故事。对于嵌套,你给每一段都加占位符:#meta.#author,并同时定义 #meta#author

名称与值,并排对照

#name:value
替换的是一个属性**名一个属性值**
映射ExpressionAttributeNamesExpressionAttributeValues
前缀#:
何时需要保留字、点、空格始终——无内联字面量
用错了哪个会报错ValidationExceptionValidationException

如果一个值被当成名称来打类型,DynamoDB 会去找一个叫 published 的属性,而你的条件永远不会按你意指的 那样匹配——所以 API 大声失败,而不是悄悄出错。那种严格是一个特性:没有安静的错误答案。

陷阱与下一步

  • 声明一个你不用的占位符——DynamoDB 拒绝任一映射里未使用的条目。从表达式构建映射,而不是先于它。
  • 编辑表达式后复用 :v——丢掉一个子句,它的值可能逗留,触发未使用条目错误。构建器让它们步调一致。
  • 因为一个名称用过一次就假设它安全——保留字冲突是按属性来的。一律加占位符,别再猜了。

这些映射出现在每条写入路径里,所以它们自然地与 单表设计以及在你附加一个过滤之前懂得 何时 Query 与 Scan配对。

Expression Builder 生成表达式外加两个映射,然后 试试 DynoTable 对你自己的表运行它们,看着占位符被解析。

更新于