iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 22 篇

Day 22|一次操作很多筆資料,怎麼確保不出錯?認識 Transaction

  • 分享至 

  • xImage
  •  

前言

前面已經開始用 TypeORM 操作 PostgreSQL,也完成了 User 的註冊流程。

剛開始寫 API 時,一個功能通常只處理一件事情,建立資料、修改資料、刪除資料,各自分開看都很單純。功能慢慢增加之後,一個 API 背後可能會接連做很多次資料庫操作,原本的一個動作,也可能變成好幾個步驟。

拿轉帳來說,A 要轉 100 元給 B,系統需要完成扣款和入帳兩件事:

A 帳戶扣 100 元
B 帳戶增加 100 元

兩個動作都完成,這筆轉帳才算成立。假設 A 已經扣掉 100 元,B 卻沒有收到,資料庫裡就會留下不完整的結果。後端很怕這種只完成一半的狀況,Transaction 就是拿來處理這類問題。

多個步驟沒有一起完成,資料就可能停在中間

資料庫裡的操作會按照順序執行,前一個操作成功之後,資料可能已經寫進資料庫,後一個操作再發生錯誤,前面的修改並不會自己消失。

轉帳可能變成:

A 帳戶扣 100 元
        ↓
成功
        ↓
B 帳戶增加 100 元
        ↓
發生錯誤

最後 A 少了 100 元,B 卻沒有增加,原本完整的一筆轉帳就被拆成了兩個結果。

註冊和建立訂單也可能碰到類似的狀況。註冊時 User 已經建立,後面的相關資料卻沒有成功;訂單建立完成,庫存卻還沒扣掉。每一步單獨看都沒有什麼問題,放進同一個流程裡,最後就可能留下不完整的資料。

有些操作本來就屬於同一件事情,不能只完成其中一段。這也是 Transaction 存在的原因。

Transaction 把原本分開的操作綁在一起

Transaction 會把多個資料庫操作放進同一組,讓它們一起完成。還是用轉帳來看:

開始 Transaction
      ↓
A 扣 100 元
      ↓
B 加 100 元
      ↓
全部成功
      ↓
保存

兩個步驟都順利完成,這次修改才會留下來。中間如果發生錯誤,整組操作就會撤回:

開始 Transaction
      ↓
A 扣 100 元
      ↓
B 加 100 元
      ↓
發生錯誤
      ↓
撤回這次操作

A 剛剛扣掉的 100 元也會一起撤回,資料就不會停在只完成一半的狀態。

Transaction 的核心概念就是把原本分開的資料庫操作綁在一起,整組成功就留下來,中間出錯就一起撤回。

成功就 Commit,失敗就 Rollback

Transaction 裡常會看到 Commit 和 Rollback 這兩個詞。Commit 代表這組操作完成,把結果保存下來;Rollback 則是在中途發生錯誤時,把這次操作撤回。

全部成功
→ Commit
→ 保存

中途失敗
→ Rollback
→ 撤回

掌握這個關係,就能看懂最基本的 Transaction 流程。

TypeORM 把 Transaction 寫進 API 裡

前面操作資料庫時,我們會直接使用 Repository,例如:

await userRepository.save(user);

當幾個資料庫操作需要一起完成,就可以使用 TypeORM 提供的 transaction(),把它們放在同一個 Transaction 裡:

await AppDataSource.transaction(async (manager) => {
  await manager.save(User, user);
  await manager.save(UserSetting, setting);
});

transaction() 裡面的操作會被放在同一個 Transaction 裡。全部順利執行,就提交這次修改;中途發生錯誤,這組資料庫操作就會撤回。

進入 Transaction 後,資料庫操作要使用傳進來的 manager,像上面的 manager.save() 就是在這個 Transaction 裡操作資料庫。現在先把 manager 想成「這次 Transaction 裡負責操作資料庫的工具」就好,其他細節留到之後再慢慢認識。

Register 從一筆資料開始,慢慢變成多個操作

前面做過的 Register,如果流程只有確認資料、Hash 密碼,再建立一筆 User,這種情況通常不需要特別加入 Transaction,因為整段流程只有一個主要的資料庫寫入操作:

const user = await userRepository.save({
  email,
  password: hashedPassword,
});

假設之後註冊流程增加了其他資料,除了建立 User,還需要一起建立一份預設設定:

建立 User
建立 User Settings

原本可能會分開執行:

const user = await userRepository.save({
  email,
  password: hashedPassword,
});

await userSettingRepository.save({
  userId: user.id,
  theme: "light",
});

第一個操作完成後,User 已經存在資料庫。第二個操作如果失敗,User 還是會留下來,註冊流程也就停在中間。

把這兩個操作放進 Transaction:

async function registerUser(
  email: string,
  password: string
) {
  const hashedPassword = await bcrypt.hash(password, 10);

  return AppDataSource.transaction(async (manager) => {
    const user = await manager.save(User, {
      email,
      password: hashedPassword,
    });

    await manager.save(UserSetting, {
      userId: user.id,
      theme: "light",
    });

    return user;
  });
}

這次註冊會先完成密碼 Hash,再進入 Transaction,接著建立 User 和 User Settings。兩個操作都成功,資料才會留下來;建立設定時發生錯誤,前面的 User 也會一起撤回。

資料越需要一起完成,越適合使用 Transaction

不是每個 API 都需要 Transaction。單純建立一筆 User,只有一個主要的資料庫操作:

await userRepository.save(user);

這種情況通常不用特別加入 Transaction。

比較適合使用 Transaction 的,是一個功能會連續修改好幾筆資料,而且少了其中一步,最後的結果就會不完整。

例如建立訂單時,可能會依序建立訂單、建立訂單明細,再扣除庫存:

建立訂單
建立訂單明細
扣除庫存

轉帳則需要同時修改兩個帳戶:

A 帳戶扣款
B 帳戶入帳

這些步驟放在一起,才算完成一件完整的事情。只要其中一個步驟失敗,就不適合讓前面的資料繼續留下來,因此這類流程就很適合使用 Transaction。

結語

剛開始寫 API 時,資料庫操作通常一個一個看,功能變多之後,才會慢慢碰到好幾個操作需要一起完成的情況。其中一步失敗,前面的資料卻還留在資料庫裡,整個流程就可能停在一個不完整的狀態。

Transaction 做的事情很單純,把這些需要一起完成的操作放在同一組,全部成功就保存,中間出錯就撤回,讓資料維持完整。

後端做到後面,要注意的就不只是功能有沒有跑完,資料最後留下來的狀態也同樣重要。


上一篇
Day 21|讓 API 只有登入的人能使用:JWT Middleware
下一篇
Day 23|知道你是誰之後,還能做什麼?Authentication vs Authorization
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言