欄位身分:顯示名稱與內部鍵
每個自訂欄位同時有「給人看的名稱」與「給系統追蹤身分的鍵」。把兩者混成同一件事,最常見的結果是欄位改名後,查詢、規則或跨表引用突然失效;分清楚後,改名就只是 UI 標籤變更。
一個欄位,兩個名稱
- 顯示名稱(display name)是使用者看到與編輯的名稱,例如
訂單狀態。可以是中文,且只需在同一張表內唯一。 - 內部鍵是建立欄位時由伺服器產生、之後不變的
col_<hex>,例如col_a1111111_1111_4111_8111_111111111111。
兩者的方向固定記在 settings.column_mapping:
{
"settings": {
"column_mapping": {
"訂單狀態": "col_a1111111_1111_4111_8111_111111111111",
"總金額": "col_b2222222_2222_4222_8222_222222222222"
}
}
}也就是規格中的 {original_name: internal_uuid_name}:key 是目前顯示名稱,value 是內部 UUID 名稱。id 是系統欄位,不會被改成 col_...。
不要自行產生 mapping
column_mapping是伺服器擁有的狀態。建表時即使傳入自己的 mapping,也不能把它當成已採用;請以建立/讀取表的回應為準。
改名為什麼不會破壞引用
假設使用者把 訂單狀態 改名為 處理狀態。伺服器只替換 mapping 的顯示名稱 key:
{
"處理狀態": "col_a1111111_1111_4111_8111_111111111111"
}內部鍵沒有變,既有資料仍存於同一個 JSON key;規則、公式、連結與查詢設定持久化的引用也仍指向同一欄。因此 React key、表單 state、快取索引與拖曳欄位 ID 都應使用 column.id/內部鍵,顯示名稱只負責標題與標籤。
寫入與讀取是刻意不對稱的
資料列寫入接受顯示名稱或已知的內部鍵。伺服器會先用當下的 column_mapping 把顯示名稱轉成內部鍵,再驗證與儲存:
{
"data": {
"訂單狀態": "待處理",
"col_b2222222_2222_4222_8222_222222222222": 1280
}
}讀取資料列時則反向映射,record.data 回傳顯示名稱,適合直接渲染:
{
"data": {
"id": "77777777-7777-4777-8777-777777777777",
"訂單狀態": "待處理",
"總金額": 1280
}
}data 裡的 id 是內部產物,不是資料列的主鍵——它永遠不等於客戶端拿在手上的頂層 record id。篩選、lookup 與 link 一律用頂層 record id;把 data.id 抄進 id 篩選什麼都比不到(見查詢 › 系統 id 欄位)。
Principal 欄位在命名之外還多一層不對稱:寫進去是身分字串(user/social_client 是原始 id,principal 是帶標籤的 user:<id>/smc:<id>/room:<id> cell),讀回來是顯示物件。讀取加工器以顯示名稱為鍵,透過 column_mapping 反查,所以即使寫入時用的是內部 col_<hex> 鍵,加工後的 cell 仍然會出現在顯示名稱底下。要把寫出去的 cell 和讀回來的 cell 對起來,請先建好反向 map。
這個便利性不代表每一個查詢欄位都接受顯示名稱。records/search 的 filters、stored_filters、sort_by 與 saved-view 內的同名欄位要求內部鍵;完整矩陣見查詢資料列。穩健的 client 應在讀表時建立正反兩張 map:
const displayToInternal = table.settings.column_mapping
const internalToDisplay = Object.fromEntries(
Object.entries(displayToInternal).map(([display, internal]) => [internal, display]),
)規則接受兩種名稱,但持久化內部鍵
設定 write rules 時,同表欄位引用可以傳顯示名稱或內部鍵。後端驗證後一律存成內部鍵,所以 when、compare、unique、require_approval 等規則在改名後仍成立。一般 GET .../rules 會把「本表」的引用轉回顯示名稱,方便編輯器呈現。
exists/not_exists 是重要例外。它們同時引用目前表與另一張 target table:
{
"type": "exists",
"table_id": "44444444-4444-4444-8444-444444444444",
"match": {
"col_c3333333_3333_4333_8333_333333333333": "客戶"
},
"where": [
{
"column": "col_d4444444_4444_4444_8444_444444444444",
"op": "eq",
"value": "有效"
}
]
}回顯時,match 的 key 與 where[].column 屬於 target table,會保留 target 的內部鍵;match value 或 $row.<欄位> 這類本表引用才會轉回本表顯示名稱。要顯示規則摘要,前端必須另外讀取 table_id 指向的目標表,再用目標表自己的 column_mapping 解碼。絕對不能拿目前表的 mapping 解 target key。
公式、連結與衍生欄也靠內部身分
link_field、target_column、rollup 的 match/filter,以及 formula expression 中的欄位引用,在 authoring payload 通常可用顯示名稱或內部鍵;伺服器解析後建立的是內部鍵依賴圖。這也是連結欄或被參照欄改名後,lookup、rollup 與公式仍能正確運作的原因。
回應為了可讀性可能把本表引用顯示成名稱;formula 遇到無法安全放回語法的名稱時,也可能保留可再次提交的內部鍵。因此編輯器應以欄位 ID 儲存 AST/選項值,不要解析回顯文字來猜身分。跨表引用更要分別保留來源與目標表 mapping。
IaC 中的身分
JSONL IaC 的 ref 是檔案內物件的宣告身分,不是 column_mapping 的替代品。伺服器套用 table/column line 後仍會產生與保存真實的 col_<hex> 鍵;export/state 才是後續演進時應沿用的伺服器狀態。
實務上應採用 export → 編輯 → plan → apply 流程,不要手寫 column_mapping 或靠顯示名稱去對照既有欄位。詳見 IaC 概念與 IaC 工作台。
前端實作檢查表
- 建表或加欄後,立即保存最新的
column_mapping。 - UI 顯示顯示名稱,但 component key 與持久化 selection 使用內部鍵。
- 資料列表單可送顯示名稱;共用 API client 仍應能把它轉成內部鍵,讓 search/view payload 一致。
- 每次欄位改名後重新抓表,不要只修改本地標題。
- 解讀跨表規則、公式與連結時,使用「被引用那張表」的 mapping。