中階閱讀時間 2 分鐘

DynamoDB 中的 Expression Attribute Name 與 Value

DynamoDB expression 是模板:你寫佔位符,然後在兩個側邊 map 中 供應真實的屬性名稱與值。#name 是一個名稱佔位符; :value 是一個值佔位符。把這兩個搞混,DynamoDB 就拒絕整個 呼叫。

DynamoDB 中的 #name:value 有什麼差別?

#name 是屬性名稱的佔位符,透過 ExpressionAttributeNames 供應;:value 是屬性值的佔位符,透過 ExpressionAttributeValues 供應。用 #name 來閃避保留字、點或空格,用 :value 來表示每個字面量 — DynamoDB 從不把值內嵌進去。兩者不可互換;把它們互調會丟出一個 ValidationException

  • #name 透過 ExpressionAttributeNames 替換一個屬性名稱 — 每當 一個屬性與保留字衝突、或含有一個點/空格時就用它。
  • :value 透過 ExpressionAttributeValues 替換一個值 — DynamoDB 從不把字面量內嵌進 expression 文字,所以每個值都是一個佔位符。
  • 它們不可互換。 在該放 : 的地方放一個 # 是一個 ValidationException,而非一個安靜的無操作。

從 SQL 過來,你把兩者都內嵌 — WHERE status = 'published'。DynamoDB 兩者都不內嵌。那個切分,正是絆倒每個新手的東西。

為什麼存在兩個 map

在 SQL 中,查詢字串承載一切:欄位名稱、字面量、運算子。 DynamoDB 刻意把 expression 的 形狀 與它的 資料 分開。

值放進它們自己的 map,這樣 DynamoDB 才能為每個型別化(SNBOOL、…), 也讓解析器永遠不必猜一個字串在哪結束 — 沒有引號 或逸出可以弄錯。完整的型別標籤清單請見 DynamoDB 中的資料型別

名稱出於另一個原因得到同樣的待遇:DynamoDB 有一長串 保留字,而任何匹配其中之一的屬性都不能在一個 expression 中以 光禿禿的名稱出現。佔位符徹底閃避了那個保留。

保留字陷阱

這裡是一個 CMS 文章表 — partition key BLOG#<blog>、sort key ARTICLE#<slug> — 它的屬性讀起來很自然,卻恰好與 保留字衝突:

屬性保留?它存什麼
statusdraft / published
name作者顯示名稱
size渲染後位元組長度
ttl封存到期(epoch)
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:expN 字串送出 — DynamoDB 的數字型別是以 字串做線上編碼。值 map 也是你跨子句重用一個值的 地方:定義 :s 一次,在一個 ConditionExpression 與一個 FilterExpression 中都參照它。

手寫這兩個 map 正是打字錯誤藏身之處。 Expression Builder 一併產生 expression 字串與兩個 map,並填好型別標籤,這樣佔位符就不會彼此 失去同步。

巢狀與彆扭路徑的名稱

# 佔位符不只是閃避保留字。document-path 語法用 點與方括號,所以一個字面上含有一個點的屬性 — 例如一個中繼資料 key og.title — 沒有佔位符就無法定址:

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

沒有它,DynamoDB 把 og.title 讀成「og map 內的 title 欄位」 — 完全不同的東西。含空格或開頭數字的名稱也是同樣的故事。 對於巢狀,你對每個區段用佔位符:#meta.#author,並把 #meta#author 兩者都定義。

名稱 vs 值,並排

#name:value
替換一個屬性 **名稱一個屬性 值**
MapExpressionAttributeNamesExpressionAttributeValues
前綴#:
何時需要保留字、點、空格永遠 — 沒有內嵌字面量
用錯時出錯ValidationExceptionValidationException

如果一個值被型別化成一個名稱,DynamoDB 會去找一個叫 published 的屬性,而你的條件絕不會以你本意的方式匹配 — 所以 API 大聲失敗。那種嚴格是一個特性:沒有安靜的錯誤答案。

陷阱與下一步

  • 宣告一個你不用的佔位符 — DynamoDB 拒絕任一 map 中未使用的 條目。從 expression 建那些 map,而非在它之前。
  • 編輯 expression 後重用 :v — 丟掉一個子句,它的值可能 滯留,觸發未使用條目錯誤。builder 把它們保持同步。
  • 因為一個名稱可行過一次就假設它安全 — 保留字衝突是 每個屬性各自的。一致地用佔位符,並停止猜測。

這些 map 出現在每條寫入路徑上,所以它們自然與 single-table design 配對,並與在你曾附加一個過濾之前就知道 何時 Query vs Scan 配對。

Expression Builder 產生 expression 加兩個 map,然後 試試 DynoTable 對你自己的表執行它們,並看著 佔位符解析。

已更新