「早安,洛基上尉。看起來你似乎遇到了問題?」
諾斯克大師走進來時,發現洛基正在電腦前皺著眉頭,螢幕上顯示著一些錯誤訊息。
洛基有些困擾地抬起頭:「大師,我想測試一個更複雜的活動資料結構,結果程式一直出錯。」
他指著螢幕:「我想儲存一個活動的完整資訊,包括座標位置、參與者名單、活動標籤等等,但 JavaScript 的資料和 DynamoDB 之間好像有些不相容...」
諾斯克大師看了看洛基的程式碼:
// 洛基嘗試儲存的複雜資料
const complexEvent = {
PK: "PLANET#MARS",
SK: "EVENT#SY210-04-20-001",
eventName: "火星天文觀測夜",
coordinates: [125.7834, 37.5665], // 經緯度座標
capacity: 100.0, // 浮點數
price: 25.99, // 價格
tags: new Set(["astronomy", "mars", "night"]), // JavaScript Set
participants: [], // 空陣列
metadata: {
// 嵌套物件
weather: "clear",
visibility: 95.5,
equipment: ["telescope", "star chart"],
},
startTime: new Date("2210-04-20T20:00:00Z"), // JavaScript Date(故事中為SY210年)
isFullyBooked: false,
organizerContact: null, // null 值
};
「我明白問題在哪了,」大師說,「你遇到的是 JavaScript 資料型別與 DynamoDB 資料型別之間的轉換問題。讓我們來深入了解這個重要的主題。」
重要提醒:以下程式碼範例中,我們使用西元年份(如 2210-04-20)來確保 JavaScript Date 物件能正確執行。在我們的星際活動系統故事中,SY210 對應西元 2210年(星際曆 = 西元年份 - 2000)。
大師在白板上畫出 DynamoDB 支援的所有資料型別:
DynamoDB 支援的資料型別:
標量型別 (Scalar Types):
- S (String) - 字串
- N (Number) - 數字(以字串表示)
- B (Binary) - 二進位資料
- BOOL (Boolean) - 布林值
- NULL - 空值
文件型別 (Document Types):
- M (Map) - 類似物件的鍵值對
- L (List) - 類似陣列的有序集合
集合型別 (Set Types):
- SS (String Set) - 字串集合
- NS (Number Set) - 數字集合
- BS (Binary Set) - 二進位集合
「重要的是,」大師強調,「JavaScript 的某些型別無法直接對應到 DynamoDB,這就是你遇到問題的原因。」
// 正確的型別對應範例
const properEventData = {
// ✓ 字串:直接對應
PK: "PLANET#MARS", // S (String)
SK: "EVENT#SY210-04-20-001", // S (String)
eventName: "火星天文觀測夜", // S (String)
// ✓ 數字:JavaScript number → DynamoDB N
capacity: 100, // N (Number)
price: 25.99, // N (Number)
// ✓ 布林值:直接對應
isFullyBooked: false, // BOOL (Boolean)
// ✓ 陣列:JavaScript Array → DynamoDB L (List)
coordinates: [125.7834, 37.5665], // L (List) [N, N]
// ✓ 物件:JavaScript Object → DynamoDB M (Map)
metadata: {
// M (Map)
weather: "clear", // S (String)
visibility: 95.5, // N (Number)
equipment: ["telescope", "star chart"], // L (List) [S, S]
},
// ✓ 字串陣列:JavaScript Array → DynamoDB SS (String Set)
tags: ["astronomy", "mars", "night"], // 會被 DocumentClient 轉為 L (List)
// ✓ 空值:JavaScript null → DynamoDB NULL
organizerContact: null, // NULL
// ✗ 問題型別需要特別處理
// Date、Set、undefined 等需要轉換
};
Hippo 的聲音響起:「看吧!我就知道會有這種問題。JavaScript 想要什麼型別都有,但 DynamoDB 可是個挑食的傢伙。」
大師開始逐一解決洛基遇到的問題:
// 問題:JavaScript Date 無法直接儲存
const problematicData = {
startTime: new Date("2210-04-20T20:00:00Z"), // ❌ 會出錯(Date物件無法直接存入DynamoDB)
};
// 解決方案 1:轉換為 ISO 字串
const solution1 = {
startTime: new Date("2210-04-20T20:00:00Z").toISOString(), // ✓ ISO 字串
};
// 解決方案 2:轉換為時間戳
const solution2 = {
startTime: Date.now(), // ✓ 數字時間戳
startTimeReadable: new Date().toISOString(), // ✓ 同時保存可讀格式
};
// 日期處理工具函數
function convertDatesForDynamoDB(obj) {
const converted = {};
for (const [key, value] of Object.entries(obj)) {
if (value instanceof Date) {
converted[key] = value.toISOString();
} else if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
converted[key] = convertDatesForDynamoDB(value); // 遞迴處理嵌套物件
} else {
converted[key] = value;
}
}
return converted;
}
// 問題:JavaScript Set 無法直接儲存
const problematicSet = {
tags: new Set(["astronomy", "mars", "night"]), // ❌ 會出錯
};
// 解決方案:根據需求選擇合適的轉換方式
// 方案 1:轉為 DynamoDB String Set(DocumentClient 會自動處理)
const stringSetSolution = {
tags: ["astronomy", "mars", "night"], // 作為陣列,DocumentClient 轉為 List
};
// 方案 2:手動建立 DynamoDB Set(需要使用原生 SDK)
const { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb");
const nativeSetSolution = {
TableName: "IntergalacticEvents",
Item: {
PK: { S: "PLANET#MARS" },
SK: { S: "EVENT#SY210-04-20-001" },
tags: { SS: ["astronomy", "mars", "night"] }, // 真正的 String Set
},
};
// Set 處理工具函數
function convertSetsForDynamoDB(obj) {
const converted = {};
for (const [key, value] of Object.entries(obj)) {
if (value instanceof Set) {
converted[key] = Array.from(value); // Set → Array
} else if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
converted[key] = convertSetsForDynamoDB(value);
} else {
converted[key] = value;
}
}
return converted;
}
// DynamoDB 的數字都以字串形式儲存,可能有精度限制
const precisionIssues = {
// ✓ 一般整數沒問題
simpleNumber: 100,
// ✓ 一般小數也 OK
decimalNumber: 25.99,
// 極大數字可能有問題
veryLargeNumber: Number.MAX_SAFE_INTEGER + 1, // 可能失去精度
// 高精度小數可能有問題
highPrecisionDecimal: 0.12345678901234567890123456789,
};
// 解決方案:對於高精度數字,考慮以字串儲存
const precisionSolution = {
largeNumberAsString: "9007199254740992", // 以字串保存大數
highPrecisionAsString: "0.123456789012345678901234567890",
// 或者分別儲存整數和小數部分
complexNumber: {
integer: 123,
decimal: "456789012345678901234567890",
isNegative: false,
},
};
// 問題:undefined 會被忽略或造成錯誤
const undefinedIssue = {
definedField: "value",
undefinedField: undefined, // 會被忽略或出錯
};
// 解決方案:明確處理 undefined
function cleanUndefinedValues(obj) {
const cleaned = {};
for (const [key, value] of Object.entries(obj)) {
if (value !== undefined) {
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value)
) {
const cleanedNested = cleanUndefinedValues(value);
if (Object.keys(cleanedNested).length > 0) {
cleaned[key] = cleanedNested;
}
} else {
cleaned[key] = value;
}
}
}
return cleaned;
}
大師給出一個完整的資料轉換解決方案:
// 完整的 DynamoDB 資料轉換工具
class DynamoDBDataConverter {
static prepare(data) {
return this.convertForDynamoDB(data);
}
static convertForDynamoDB(obj) {
if (obj === null || obj === undefined) {
return null;
}
if (obj instanceof Date) {
return obj.toISOString();
}
if (obj instanceof Set) {
return Array.from(obj);
}
if (Array.isArray(obj)) {
return obj.map((item) => this.convertForDynamoDB(item));
}
if (typeof obj === "object") {
const converted = {};
for (const [key, value] of Object.entries(obj)) {
// 跳過 undefined 值
if (value !== undefined) {
converted[key] = this.convertForDynamoDB(value);
}
}
return converted;
}
// 基本型別直接返回
return obj;
}
static convertFromDynamoDB(obj) {
if (obj === null || obj === undefined) {
return obj;
}
if (typeof obj === "string") {
// 嘗試轉換 ISO 日期字串
if (this.isISODateString(obj)) {
return new Date(obj);
}
}
if (Array.isArray(obj)) {
return obj.map((item) => this.convertFromDynamoDB(item));
}
if (typeof obj === "object") {
const converted = {};
for (const [key, value] of Object.entries(obj)) {
converted[key] = this.convertFromDynamoDB(value);
}
return converted;
}
return obj;
}
static isISODateString(str) {
const isoRegex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
return isoRegex.test(str) && !isNaN(Date.parse(str));
}
// 驗證資料是否適合 DynamoDB
static validate(data) {
const errors = [];
this.validateRecursive(data, "", errors);
return {
isValid: errors.length === 0,
errors: errors,
};
}
static validateRecursive(obj, path, errors) {
if (obj === null || obj === undefined) {
return;
}
if (obj instanceof Date) {
errors.push(`${path}: Date 物件需要轉換為字串`);
return;
}
if (obj instanceof Set) {
errors.push(`${path}: Set 物件需要轉換為陣列`);
return;
}
if (typeof obj === "function") {
errors.push(`${path}: 函數無法儲存到 DynamoDB`);
return;
}
if (typeof obj === "symbol") {
errors.push(`${path}: Symbol 無法儲存到 DynamoDB`);
return;
}
if (Array.isArray(obj)) {
obj.forEach((item, index) => {
this.validateRecursive(item, `${path}[${index}]`, errors);
});
return;
}
if (typeof obj === "object") {
for (const [key, value] of Object.entries(obj)) {
const newPath = path ? `${path}.${key}` : key;
this.validateRecursive(value, newPath, errors);
}
}
}
}
洛基看著這個完整的解決方案,開始測試他原本有問題的資料:
// 洛基的原始複雜資料(有問題的版本)
const originalComplexEvent = {
PK: "PLANET#MARS",
SK: "EVENT#SY210-04-20-001",
eventName: "火星天文觀測夜",
coordinates: [125.7834, 37.5665],
capacity: 100.0,
price: 25.99,
tags: new Set(["astronomy", "mars", "night"]), // ❌ Set
participants: [],
metadata: {
weather: "clear",
visibility: 95.5,
equipment: ["telescope", "star chart"],
lastUpdated: new Date(), // ❌ Date
},
startTime: new Date("2210-04-20T20:00:00Z"), // ❌ Date
isFullyBooked: false,
organizerContact: null,
undefinedField: undefined, // ❌ undefined
};
// 使用轉換工具
async function saveComplexEvent() {
try {
// 1. 驗證資料
const validation = DynamoDBDataConverter.validate(originalComplexEvent);
if (!validation.isValid) {
console.log("資料驗證錯誤:", validation.errors);
}
// 2. 轉換資料
const convertedEvent = DynamoDBDataConverter.prepare(originalComplexEvent);
console.log("轉換後的資料:", JSON.stringify(convertedEvent, null, 2));
// 3. 儲存到 DynamoDB
const command = new PutCommand({
TableName: "IntergalacticEvents",
Item: convertedEvent,
});
await docClient.send(command);
console.log("複雜活動資料儲存成功!");
// 4. 讀取並轉換回來
const getCommand = new GetCommand({
TableName: "IntergalacticEvents",
Key: {
PK: convertedEvent.PK,
SK: convertedEvent.SK,
},
});
const result = await docClient.send(getCommand);
if (result.Item) {
const restoredEvent = DynamoDBDataConverter.convertFromDynamoDB(
result.Item
);
console.log("恢復的資料:", restoredEvent);
console.log("日期是否正確恢復:", restoredEvent.startTime instanceof Date);
}
} catch (error) {
console.error("操作失敗:", error.message);
}
}
saveComplexEvent();
洛基執行程式碼後,興奮地說:「太神奇了!現在我可以處理各種複雜的資料結構了!」
大師接著展示一些進階的資料型別應用:
// 處理二進位資料(如圖片、文件等)
const binaryDataExample = {
PK: "FILE#001",
SK: "METADATA",
fileName: "mars-landscape.jpg",
fileSize: 1024000,
// 小檔案可以直接儲存(建議小於 400KB)
fileData: Buffer.from("fake-binary-data"), // B (Binary)
// 大檔案建議儲存在 S3,這裡只存連結
s3Location: "s3://space-images/mars-landscape.jpg",
contentType: "image/jpeg",
};
// 使用原生 SDK 來建立真正的 Set
const { DynamoDBClient, PutItemCommand } = require("@aws-sdk/client-dynamodb");
const setExample = {
TableName: "IntergalacticEvents",
Item: {
PK: { S: "EVENT#001" },
SK: { S: "PARTICIPANTS" },
// 字串集合:參與者 ID 列表
participantIds: {
SS: ["USER#001", "USER#002", "USER#003"], // String Set
},
// 數字集合:評分列表
ratings: {
NS: ["5", "4", "5", "3"], // Number Set(注意:數字以字串形式)
},
},
};
// 使用 DocumentClient 的 Set 型別
const { DynamoDBDocumentClient } = require("@aws-sdk/lib-dynamodb");
const docClientSetExample = {
PK: "EVENT#001",
SK: "PARTICIPANTS",
// DocumentClient 支援的 Set 語法
participantIds: docClient.createSet(["USER#001", "USER#002", "USER#003"]),
ratings: docClient.createSet([5, 4, 5, 3]),
};
// 大型嵌套物件的處理
const largeObjectExample = {
PK: "SURVEY#001",
SK: "RESPONSES",
// 問題:物件太大(接近 400KB 限制)
surveyResponses: {
// ... 大量資料
},
};
// 解決策略 1:分拆儲存
const splitStorageStrategy = [
{
PK: "SURVEY#001",
SK: "RESPONSE#BATCH#001",
responses: {
/* 第一批回應 */
},
},
{
PK: "SURVEY#001",
SK: "RESPONSE#BATCH#002",
responses: {
/* 第二批回應 */
},
},
];
// 解決策略 2:壓縮儲存
const compressedStorage = {
PK: "SURVEY#001",
SK: "RESPONSES#COMPRESSED",
compressedData: "compressed-string-representation",
compressionType: "gzip",
originalSize: 500000,
compressedSize: 50000,
};
洛基認真地說:「資料型別的處理確實比我想像中複雜,但這些工具讓一切變得可控了。」
諾斯克大師點頭:「理解資料型別是成為優秀 DynamoDB 開發者的關鍵。不同的資料型別有不同的效能特性和使用場景。」
Hippo 補充道:「記住,菜鳥,資料型別不只是技術問題,也是設計問題。選擇正確的資料型別能讓你的系統更高效、更穩定。」
諾斯克大師說:「明天我們來學習一個實用的進階主題:如何實現完整的分頁功能。你會發現,在實際應用中,分頁不只是概念,更需要完整的程式碼實現。」
時間設定說明:故事中使用星際曆(SY210 = 西元2210年),程式碼範例為確保正確執行,使用對應的西元年份。