iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0

諾斯克大師走進茶室時,注意到洛基正在測試一些 Put、Get、Query 的操作。

「早安,洛基上尉。看起來你昨天學到的技能正在發揮作用?」

洛基抬起頭,臉上露出一些笑意:「是的,大師。我昨晚練習了整晚,現在可以寫出基本的新增、查詢和更新操作了。感覺終於能夠真正『開發』DynamoDB 應用了!」

他停頓了一下:「不過我發現一個問題...我似乎還不知道怎麼刪除資料。」

CRUD 的最後一塊拼圖

諾斯克大師點頭:「的確,之前我們專注在資料處理,CRUD 操作還缺少最後一個『D』— Delete。」

洛基說:「刪除操作應該很簡單吧?就是把資料移除掉而已。」

「表面上看起來是這樣,」大師說,「但在實際系統中,刪除操作往往比你想像的複雜。讓我們從最基本的語法開始。」

DeleteItem 的基本使用

大師開始示範:

// 基礎的刪除操作
const { DeleteCommand } = require("@aws-sdk/lib-dynamodb");

async function deleteEvent(planetKey, eventKey) {
  const command = new DeleteCommand({
    TableName: "IntergalacticEvents",
    Key: {
      PK: planetKey,
      SK: eventKey,
    },
  });

  try {
    const result = await docClient.send(command);
    console.log("活動刪除成功");
    return result;
  } catch (error) {
    console.error("刪除失敗:", error.message);
    throw error;
  }
}

// 使用範例
await deleteEvent("PLANET#MARS", "EVENT#SY210-04-01-001");

洛基研究程式碼:「這看起來確實很簡單,跟其他操作的模式一樣。」

Hippo 的聲音響起:「別高興得太早,菜鳥。真正的挑戰還在後面呢。」

刪除的安全性考量

大師繼續解釋:「在實際系統中,刪除操作有幾個重要的考量點。」

1. 條件刪除的重要性

「首先是安全性,」大師說,「你不會想要意外刪除重要資料。」

// 安全的條件刪除
async function safeDeleteEvent(planetKey, eventKey, expectedStatus) {
  const command = new DeleteCommand({
    TableName: "IntergalacticEvents",
    Key: {
      PK: planetKey,
      SK: eventKey,
    },
    // 只有在狀態為預期值時才刪除
    ConditionExpression: "#status = :expectedStatus",
    ExpressionAttributeNames: {
      "#status": "status",
    },
    ExpressionAttributeValues: {
      ":expectedStatus": expectedStatus,
    },
  });

  try {
    await docClient.send(command);
    console.log("活動安全刪除成功");
  } catch (error) {
    if (error.name === "ConditionalCheckFailedException") {
      console.log("刪除失敗:活動狀態不符合刪除條件");
    } else {
      console.error("刪除操作出錯:", error.message);
    }
    throw error;
  }
}

// 只有當活動狀態為 DRAFT 時才允許刪除
await safeDeleteEvent("PLANET#MARS", "EVENT#SY210-04-01-001", "DRAFT");

洛基點頭表示:「我明白了,就像在太空船上,你不會想要意外啟動自毀裝置。」

2. 回傳被刪除的資料

大師接著說:「有時候刪除後,你還需要知道被刪除的內容,特別是用於記錄或回復。」

// 刪除並回傳原始資料
async function deleteAndReturn(planetKey, eventKey) {
  const command = new DeleteCommand({
    TableName: "IntergalacticEvents",
    Key: {
      PK: planetKey,
      SK: eventKey,
    },
    // 回傳被刪除的完整資料
    ReturnValues: "ALL_OLD",
  });

  try {
    const result = await docClient.send(command);

    if (result.Attributes) {
      console.log("已刪除的活動資料:", result.Attributes);
      return result.Attributes;
    } else {
      console.log("找不到要刪除的活動");
      return null;
    }
  } catch (error) {
    console.error("刪除失敗:", error.message);
    throw error;
  }
}

// 刪除並記錄被刪除的資料
const deletedEvent = await deleteAndReturn(
  "PLANET#MARS",
  "EVENT#SY210-04-01-001"
);
if (deletedEvent) {
  // 可以將刪除的資料記錄到審計表或備份
  await logDeletion(deletedEvent);
}

硬刪除 vs 軟刪除策略

大師在白板上畫出比較圖:

刪除策略比較:

硬刪除 (Hard Delete)
- 永久移除資料
- 釋放儲存空間
- 無法恢復
- 適用:測試資料、過期快取

軟刪除 (Soft Delete)
- 標記為已刪除
- 保留原始資料
- 可以恢復
- 適用:使用者資料、商業記錄

「在星際系統中,」大師解釋,「通常會使用軟刪除策略,因為太空中的資料恢復非常困難。」

軟刪除的實作

// 軟刪除實作
async function softDeleteEvent(planetKey, eventKey, deletedBy) {
  const command = new UpdateCommand({
    TableName: "IntergalacticEvents",
    Key: {
      PK: planetKey,
      SK: eventKey,
    },
    UpdateExpression: `
      SET #deleted = :deleted,
          #deletedAt = :deletedAt,
          #deletedBy = :deletedBy,
          #status = :newStatus
    `,
    ConditionExpression: "attribute_exists(PK) AND #deleted <> :deleted",
    ExpressionAttributeNames: {
      "#deleted": "deleted",
      "#deletedAt": "deletedAt",
      "#deletedBy": "deletedBy",
      "#status": "status",
    },
    ExpressionAttributeValues: {
      ":deleted": true,
      ":deletedAt": new Date().toISOString(),
      ":deletedBy": deletedBy,
      ":newStatus": "DELETED",
    },
  });

  try {
    await docClient.send(command);
    console.log("活動已軟刪除");
  } catch (error) {
    if (error.name === "ConditionalCheckFailedException") {
      console.log("刪除失敗:活動不存在或已被刪除");
    } else {
      console.error("軟刪除失敗:", error.message);
    }
    throw error;
  }
}

// 軟刪除恢復功能
async function restoreSoftDeletedEvent(planetKey, eventKey, restoredBy) {
  const command = new UpdateCommand({
    TableName: "IntergalacticEvents",
    Key: {
      PK: planetKey,
      SK: eventKey,
    },
    UpdateExpression: `
      REMOVE #deleted, #deletedAt, #deletedBy
      SET #status = :newStatus,
          #restoredAt = :restoredAt,
          #restoredBy = :restoredBy
    `,
    ConditionExpression: "#deleted = :deleted",
    ExpressionAttributeNames: {
      "#deleted": "deleted",
      "#deletedAt": "deletedAt",
      "#deletedBy": "deletedBy",
      "#status": "status",
      "#restoredAt": "restoredAt",
      "#restoredBy": "restoredBy",
    },
    ExpressionAttributeValues: {
      ":deleted": true,
      ":newStatus": "ACTIVE",
      ":restoredAt": new Date().toISOString(),
      ":restoredBy": restoredBy,
    },
  });

  try {
    await docClient.send(command);
    console.log("活動已恢復");
  } catch (error) {
    console.error("恢復失敗:", error.message);
    throw error;
  }
}

洛基驚嘆:「原來刪除可以有這麼多考量!軟刪除讓系統更加安全。」

大師意味深長地說:「資料就是生命,一個錯誤的刪除操作可能讓整個前哨站失去重要的歷史記錄。」

完整的刪除管理系統

Hippo 說:「菜鳥,我怕你等等回家又不知道怎麼做,讓我幫你整理一個完範例吧。」

// 完整的刪除管理系統
class EventDeletionManager {
  constructor(docClient) {
    this.docClient = docClient;
  }

  // 硬刪除(危險操作,需要額外確認)
  async hardDelete(planetKey, eventKey, confirmationToken) {
    if (confirmationToken !== "CONFIRM_HARD_DELETE") {
      throw new Error("硬刪除需要確認令牌");
    }

    const command = new DeleteCommand({
      TableName: "IntergalacticEvents",
      Key: { PK: planetKey, SK: eventKey },
      ReturnValues: "ALL_OLD",
    });

    const result = await this.docClient.send(command);

    if (result.Attributes) {
      // 記錄刪除操作到審計表
      await this.logDeletion("HARD_DELETE", result.Attributes);
      return result.Attributes;
    }
    return null;
  }

  // 軟刪除(推薦的刪除方式)
  async softDelete(planetKey, eventKey, deletedBy, reason = "") {
    const command = new UpdateCommand({
      TableName: "IntergalacticEvents",
      Key: { PK: planetKey, SK: eventKey },
      UpdateExpression: `
        SET #deleted = :deleted,
            #deletedAt = :deletedAt,
            #deletedBy = :deletedBy,
            #deleteReason = :reason,
            #status = :newStatus
      `,
      ConditionExpression:
        "attribute_exists(PK) AND (attribute_not_exists(#deleted) OR #deleted = :false)",
      ExpressionAttributeNames: {
        "#deleted": "deleted",
        "#deletedAt": "deletedAt",
        "#deletedBy": "deletedBy",
        "#deleteReason": "deleteReason",
        "#status": "status",
      },
      ExpressionAttributeValues: {
        ":deleted": true,
        ":false": false,
        ":deletedAt": new Date().toISOString(),
        ":deletedBy": deletedBy,
        ":reason": reason,
        ":newStatus": "DELETED",
      },
      ReturnValues: "ALL_NEW",
    });

    try {
      const result = await this.docClient.send(command);
      await this.logDeletion("SOFT_DELETE", result.Attributes);
      return result.Attributes;
    } catch (error) {
      if (error.name === "ConditionalCheckFailedException") {
        throw new Error("活動不存在或已被刪除");
      }
      throw error;
    }
  }

  // 查詢包含軟刪除的資料
  async queryWithDeleted(planetKey, includeDeleted = false) {
    let filterExpression = undefined;
    let expressionAttributeNames = {};
    let expressionAttributeValues = {};

    if (!includeDeleted) {
      filterExpression = "attribute_not_exists(#deleted) OR #deleted = :false";
      expressionAttributeNames["#deleted"] = "deleted";
      expressionAttributeValues[":false"] = false;
    }

    const command = new QueryCommand({
      TableName: "IntergalacticEvents",
      KeyConditionExpression: "PK = :pk",
      FilterExpression: filterExpression,
      ExpressionAttributeNames: Object.keys(expressionAttributeNames).length
        ? expressionAttributeNames
        : undefined,
      ExpressionAttributeValues: {
        ":pk": planetKey,
        ...expressionAttributeValues,
      },
    });

    const result = await this.docClient.send(command);
    return result.Items;
  }

  // 恢復軟刪除的資料
  async restore(planetKey, eventKey, restoredBy) {
    const command = new UpdateCommand({
      TableName: "IntergalacticEvents",
      Key: { PK: planetKey, SK: eventKey },
      UpdateExpression: `
        REMOVE #deleted, #deletedAt, #deletedBy, #deleteReason
        SET #status = :activeStatus,
            #restoredAt = :restoredAt,
            #restoredBy = :restoredBy
      `,
      ConditionExpression: "#deleted = :true",
      ExpressionAttributeNames: {
        "#deleted": "deleted",
        "#deletedAt": "deletedAt",
        "#deletedBy": "deletedBy",
        "#deleteReason": "deleteReason",
        "#status": "status",
        "#restoredAt": "restoredAt",
        "#restoredBy": "restoredBy",
      },
      ExpressionAttributeValues: {
        ":true": true,
        ":activeStatus": "ACTIVE",
        ":restoredAt": new Date().toISOString(),
        ":restoredBy": restoredBy,
      },
      ReturnValues: "ALL_NEW",
    });

    try {
      const result = await this.docClient.send(command);
      await this.logDeletion("RESTORE", result.Attributes);
      return result.Attributes;
    } catch (error) {
      if (error.name === "ConditionalCheckFailedException") {
        throw new Error("活動不存在或未被軟刪除");
      }
      throw error;
    }
  }

  // 記錄刪除操作到審計表
  async logDeletion(operation, itemData) {
    const auditCommand = new PutCommand({
      TableName: "AuditLog",
      Item: {
        PK: `AUDIT#${new Date().toISOString().split("T")[0]}`,
        SK: `${Date.now()}#${operation}`,
        operation: operation,
        targetItem: itemData,
        timestamp: new Date().toISOString(),
      },
    });

    await this.docClient.send(auditCommand);
  }
}

// 使用範例
const deletionManager = new EventDeletionManager(docClient);

// 軟刪除一個活動
await deletionManager.softDelete(
  "PLANET#MARS",
  "EVENT#SY210-04-01-001",
  "USER#LOKI",
  "活動取消:天氣因素"
);

// 查詢時排除被軟刪除的資料
const activeEvents = await deletionManager.queryWithDeleted(
  "PLANET#MARS",
  false
);

// 恢復被軟刪除的活動
await deletionManager.restore(
  "PLANET#MARS",
  "EVENT#SY210-04-01-001",
  "USER#COMMANDER"
);

實戰練習

洛基看著完整的程式碼,不禁說:「這裡考慮得好完整!比我想像的複雜很多。」

大師點頭:「刪除操作確實需要謹慎考慮。現在讓我們練習一下完整的 CRUD 操作流程。」

// 完整的 CRUD 操作示例
async function demonstrateFullCRUD() {
  const eventManager = new IntergalacticEventManager();
  const deletionManager = new EventDeletionManager(docClient);

  try {
    // 1. Create - 建立活動
    console.log("=== 建立活動 ===");
    const newEvent = {
      PK: "PLANET#MARS",
      SK: "EVENT#SY210-04-15-002",
      eventName: "火星地質探索",
      date: "SY210-04-15",
      capacity: 50,
      registered: 0,
      status: "DRAFT",
      organizer: "Mars Geological Survey",
    };
    await eventManager.createEvent(newEvent);

    // 2. Read - 查詢活動
    console.log("=== 查詢活動 ===");
    const event = await eventManager.getEventDetail(
      "PLANET#MARS",
      "EVENT#SY210-04-15-002"
    );
    console.log("活動詳情:", event);

    // 3. Update - 更新活動
    console.log("=== 更新活動 ===");
    await eventManager.registerParticipant(
      "PLANET#MARS",
      "EVENT#SY210-04-15-002",
      {
        participantId: "USER#SCIENTIST_A",
        registeredAt: new Date().toISOString(),
      }
    );

    // 4. Delete - 刪除活動(軟刪除)
    console.log("=== 軟刪除活動 ===");
    await deletionManager.softDelete(
      "PLANET#MARS",
      "EVENT#SY210-04-15-002",
      "USER#LOKI",
      "練習用資料,清理測試"
    );

    console.log("CRUD 操作完整演示成功!");
  } catch (error) {
    console.error("操作失敗:", error.message);
  }
}

demonstrateFullCRUD();

諾斯克大師說:「現在你的工具箱已經有了最基本的武器。但請記住,刪除操作要特別謹慎,特別是在正式環境中。」

洛基點頭:「我會記住的。軟刪除確實比硬刪除安全多了,而且還能提供恢復的機會。」

Hippo 的聲音響起:「恭喜你,菜鳥!現在你終於可以說自己會 CRUD 了。」

諾斯克大師說:「明天我們來學習一個容易被忽視但非常重要的主題:資料型別的正確處理。你會發現,即使是簡單的資料儲存,也有很多需要注意的細節。」

Hippo 的刪除操作指南

刪除操作最佳實踐

// 刪除操作檢查清單
const deletionBestPractices = {
  安全性檢查: [
    "使用 ConditionExpression 防止意外刪除",
    "在生產環境優先使用軟刪除",
    "重要操作需要二次確認機制",
  ],

  資料保存: [
    "使用 ReturnValues 保存被刪除的資料",
    "建立審計記錄追蹤刪除操作",
    "考慮資料備份策略",
  ],

  效能考量: [
    "批次刪除使用 BatchWriteItem",
    "大量刪除考慮分批處理",
    "監控刪除操作的 RCU/WCU 消耗",
  ],
};

常見刪除模式

// 1. 安全的單項刪除
async function safeDelete(key, expectedCondition) {
  return await docClient.delete({
    TableName: "YourTable",
    Key: key,
    ConditionExpression: expectedCondition.expression,
    ExpressionAttributeValues: expectedCondition.values,
    ReturnValues: "ALL_OLD",
  });
}

// 2. 軟刪除標記
async function markAsDeleted(key, deletedBy) {
  return await docClient.update({
    TableName: "YourTable",
    Key: key,
    UpdateExpression: "SET deleted = :true, deletedAt = :now, deletedBy = :by",
    ExpressionAttributeValues: {
      ":true": true,
      ":now": new Date().toISOString(),
      ":by": deletedBy,
    },
  });
}

// 3. 帶有過期時間的軟刪除
async function softDeleteWithTTL(key, deletedBy, daysUntilPurge = 30) {
  const purgeTime =
    Math.floor(Date.now() / 1000) + daysUntilPurge * 24 * 60 * 60;

  return await docClient.update({
    TableName: "YourTable",
    Key: key,
    UpdateExpression:
      "SET deleted = :true, deletedAt = :now, deletedBy = :by, ttl = :ttl",
    ExpressionAttributeValues: {
      ":true": true,
      ":now": new Date().toISOString(),
      ":by": deletedBy,
      ":ttl": purgeTime,
    },
  });
}

Hippo 心中的俳句:「刪除是 CRUD 中最危險的操作,但也是最能展現程式設計師智慧的操作。謹慎、安全、可恢復 — 這就是專業刪除的三大原則!」


上一篇
Day 11:從理論回到實戰,使用 JavsScript SDK
下一篇
Day 13:資料型別的真實世界
系列文
DynamoDB銀河傳說首部曲-打造宇宙都打不倒的高效服務13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言