iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0

「早安,洛基上尉。看起來你似乎遇到了問題?」

諾斯克大師走進來時,發現洛基正在電腦前皺著眉頭,螢幕上顯示著一些錯誤訊息。

洛基有些困擾地抬起頭:「大師,我想測試一個更複雜的活動資料結構,結果程式一直出錯。」

他指著螢幕:「我想儲存一個活動的完整資訊,包括座標位置、參與者名單、活動標籤等等,但 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 支援的所有資料型別:

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,這就是你遇到問題的原因。」

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 可是個挑食的傢伙。」

常見型別轉換問題與解決方案

大師開始逐一解決洛基遇到的問題:

1. JavaScript Date 的處理

// 問題: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;
}

2. JavaScript Set 的處理

// 問題: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;
}

3. 數字精度問題

// 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,
  },
};

4. undefined 值的處理

// 問題: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();

洛基執行程式碼後,興奮地說:「太神奇了!現在我可以處理各種複雜的資料結構了!」

特殊資料型別的進階應用

大師接著展示一些進階的資料型別應用:

1. 二進位資料的處理

// 處理二進位資料(如圖片、文件等)
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",
};

2. 字串集合與數字集合

// 使用原生 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]),
};

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年),程式碼範例為確保正確執行,使用對應的西元年份。


上一篇
Day 12:補完 CRUD 失落的一片-刪除
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言