中階閱讀時間 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 把運算式字串和兩個對映一起生成,型別標籤都已填好,所以預留位置無法彼此失同步。

下面這個構建器在 status——一個保留字——上做篩選,你可以看到它在 ExpressionAttributeNames 對映裡被自動別名為 #status

建構你的請求
產生的程式碼
new QueryCommand({
  "TableName": "AuditLog",
  "KeyConditionExpression": "#hashKey = :hashKeyValue",
  "FilterExpression": "#filter0 = :filterValue0",
  "ExpressionAttributeNames": {
    "#hashKey": "pk",
    "#filter0": "status"
  },
  "ExpressionAttributeValues": {
    ":hashKeyValue": {
      "S": "TENANT#acme"
    },
    ":filterValue0": {
      "S": "active"
    }
  }
})

用名稱處理巢狀和彆扭的路徑

# 預留位置的作用不止於避開保留字。文件路徑語法用點和方括號,所以一個字面包含一個點的屬性——比如一個 後設資料鍵 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 對你自己的表執行它們,看著預留位置被解析。

已更新