在資料庫中有一個術語為 ACID,它其實是資料庫管理系統為保證數據可靠的四個特性的縮寫,分別為「原子性 Atomicity」、「一致性 Consistency」、「隔離 Isolation」與「永久性 Durability」。而我們現在要用到的「交易 Transaction」功能主要是原子性 Atomicity 的應用,簡單一句話來說明這個特性就是「全有,或全無」。
為什麼要使用交易?你可以嘗試用任何程式語言寫一個迴圈,這個迴圈會逐步將多筆資料塞入陣列,且迴圈中間發生了寫入錯誤 (例如:Index 無效) 造成執行中斷,而如果你的這個陣列內容是會直接序列化的話 (例如:Unity 的 ScriptableObject),就會發現裡面會存在程式中斷前已寫入的不完整資料。那假設我們要再次嘗試寫入資料的話要怎麼做?可能做法就是直接 Clear 陣列重來,否則就需要先讀取陣列內容以防止重複資料。資料庫的寫入或更動資料其實也是如此,如果沒有交易,好好的寫入作業被中斷就會造成數據不完整的問題,所以才需要交易功能的原子性來解決它。
「全有,或全無」的特性可以用一個範例來舉例:
假設我們現在有一個空資料表,現在要一次建立多筆產品資料 Product,主鍵為 ID 且不可重複,但這邊假設提供的內容有重複的可能性。
一、建立產品的資料模型
public class Product {
[BsonId]
[BsonRepresentation(BsonType.Int32)]
public int Id { get; set; }
public string Name { get; set; }
public float Price { get; set; }
}
二、建立 MongoDB 資料庫連線,並先取好資料庫與集合 Collection。
var connectionString = "mongodb://admin:[email protected]:27017";
var client = new MongoClient(connectionString);
var collection = client.GetDatabase("MyStore").GetCollection<Product>("Product");
三、完成迴圈寫入與交易功能實作。
public async Task CreateProducts (Product[] products) {
// 啟動會話
using (var session = await client.StartSessionAsync())
{
// 開始交易
session.StartTransaction();
try
{
// 執行寫入
foreach(var e in products)
{
// 寫入資料,可能會出錯
await collection.InsertOneAsync(session, e);
}
// 無中斷視為成功,確認交易
await session.CommitTransactionAsync();
}
catch (Exception)
{
// 攔截錯誤,將已寫入的不完整資料全部回滾至寫入前狀態
await session.AbortTransactionAsync();
}
}
}
在任何可能發生資料寫入問題的地方基本上都需要實作交易,僅查詢的話不需要交易。
來說明一下這支程式的原理:
在需要交易的區塊必須先啟動一個會話 Session,它會給你一個 IClientSessionHandle。這邊我們可以用一個很簡單的 using 把區塊包起來,在離開區塊時會自動 Dispose 以防止記憶體洩漏 Memory Leak。
client.StartSessionAsync() 的 client 是最前面建立資料庫連線 new MongoClient() 的物件,需要用它來啟動會話。這邊我有看到有人在 ASP.NET 中用 DI 來實作,只不過因為我有用到多資料庫的關係不好實作,就先暫時不講那個方法。
using (var session = await client.StartSessionAsync()) {
// TODO
}
啟動會話後,就可以在會話有效區域內啟動交易,在上面這個區塊內的最前面加入:
session.StartTransaction();
接下來加入最主要的迴圈寫入程式:
foreach(var e in products)
{
await collection.InsertOneAsync(session, e);
}
這邊要注意 InsertOneAsync 方法內的參數,平常沒有交易的寫法是直接塞入 Object,但現在因為要使用交易功能則必須在第一個參數寫入會話的 session。印象中這版的 MongoDB C# 所有會關係刪改的方法應該都有支援交易 (有誤請指正,這個我不太確定)。
// 無交易
await collection.InsertOneAsync(e);
// 有交易
await collection.InsertOneAsync(session, e);
前面我們有提到,假設在執行 Insert 時會發生錯誤,為了不使程式直接 Crash,需要用 try-catch 攔截它,需要改寫成下面這樣:
try
{
foreach(var e in products)
{
await collection.InsertOneAsync(session, e);
}
}
catch (Exception)
{
// 攔截錯誤
}
最後加入最關鍵的兩個方法:「確認」與「回滾」。當呼叫確認時,在交易中所有的刪改都會真正的寫入資料庫,反過來說,如果你沒有呼叫確認,你前面的所作所為就是做白工;而當呼叫回滾時,交易中刪改皆會被回滾至作業開始前的狀態。
有沒有發現這就是原子性的「全有,或全無」呢?
// 確認
await session.CommitTransactionAsync();
// 回滾
await session.AbortTransactionAsync();
插入確認與回滾方法後的內容長這樣:
try
{
foreach(var e in products)
{
await collection.InsertOneAsync(session, e);
}
await session.CommitTransactionAsync();
}
catch (Exception)
{
await session.AbortTransactionAsync();
}
以上。
參考資料
- ACID (維基百科)
- 關於SQL Transaction (Han)
- Day 2 – 我的C是你的C嗎,介紹CAP Theorem與ACID/BASE (Jack Lin)
- [Day 16] Database Transaction & ACID – (1) (莫力全 Kyle Mo)
- Getting started with .NET Core API, MongoDB, and Transactions (Alex Alves)
- Multiple document transaction not working in c# using mongodb 4.08 community server (Stackoverflow)
- Working with MongoDB Transactions with C# and the .NET Framework