諾斯克大師走進茶室時,注意到洛基正在測試一些 Put、Get、Query 的操作。
「早安,洛基上尉。看起來你昨天學到的技能正在發揮作用?」
洛基抬起頭,臉上露出一些笑意:「是的,大師。我昨晚練習了整晚,現在可以寫出基本的新增、查詢和更新操作了。感覺終於能夠真正『開發』DynamoDB 應用了!」
他停頓了一下:「不過我發現一個問題...我似乎還不知道怎麼刪除資料。」
諾斯克大師點頭:「的確,之前我們專注在資料處理,CRUD 操作還缺少最後一個『D』— Delete。」
洛基說:「刪除操作應該很簡單吧?就是把資料移除掉而已。」
「表面上看起來是這樣,」大師說,「但在實際系統中,刪除操作往往比你想像的複雜。讓我們從最基本的語法開始。」
大師開始示範:
// 基礎的刪除操作
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 的聲音響起:「別高興得太早,菜鳥。真正的挑戰還在後面呢。」
大師繼續解釋:「在實際系統中,刪除操作有幾個重要的考量點。」
「首先是安全性,」大師說,「你不會想要意外刪除重要資料。」
// 安全的條件刪除
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");
洛基點頭表示:「我明白了,就像在太空船上,你不會想要意外啟動自毀裝置。」
大師接著說:「有時候刪除後,你還需要知道被刪除的內容,特別是用於記錄或回復。」
// 刪除並回傳原始資料
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);
}
大師在白板上畫出比較圖:
刪除策略比較:
硬刪除 (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 了。」
諾斯克大師說:「明天我們來學習一個容易被忽視但非常重要的主題:資料型別的正確處理。你會發現,即使是簡單的資料儲存,也有很多需要注意的細節。」
// 刪除操作檢查清單
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 中最危險的操作,但也是最能展現程式設計師智慧的操作。謹慎、安全、可恢復 — 這就是專業刪除的三大原則!」