[.NET C#] MongoDB 5.0 資料庫交易 (Transaction) 語法筆記

在資料庫中有一個術語為 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();
}

以上。

參考資料

發佈留言