日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > C# >内容正文

C#

C# 从代码入门 Mysql 数据库事务

發布時間:2023/12/24 C# 40 coder
生活随笔 收集整理的這篇文章主要介紹了 C# 从代码入门 Mysql 数据库事务 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

目錄
  • 生成數據庫數據
  • Mysql 數據庫事務基礎
    • 數據庫的并發一致性問題
    • 數據庫事務的隔離級別
  • BeginTransaction() 和 TransactionScope 的區別
    • BeginTransaction()
    • 可以不手動撤銷
    • TransactionScope
    • 總結
  • DML 是否可以使用事務
  • 順序多操作
  • 嵌套事務
  • 事務范圍
  • 封裝 DbContext
    • TransactionScope
    • BeginTransaction()

在業務開發中,使用數據庫事務是必不可少的。而開發中往往會使用各種 ORM 執行數據庫操作,簡化代碼復雜度,不過,由于各種 ORM 的封裝特性,開發者的使用方式也不一樣,開發者想要了解 ORM 對事務做了什么處理是比較難的。因此,本文介紹數據庫事務基礎、Ado.net 事務、如何封裝 DbContext ,讀者掌握以后,可以加深對 C# 使用事務的理解,使用各種 ORM 時也會更應手。

生成數據庫數據

為了演示各種事務操作,我們想要先創建 demo 數據,打開 filldb 官網,根據操作提示生成模擬數據。

filldb 地址: https://filldb.info/dummy/step1

FillDB 是一款免費工具,可快速生成大量 MySql 格式的自定義數據,用于測試軟件和使用隨機數據填充數據庫。

然后按照 authors、posts 的順序,點擊 Generate ,生成數據庫數據。

因為 posts 有 authors 的外鍵,因此生成數據的順序是 authors、posts。

最后點擊 Export database 導出 SQL 即可。

然后在數據庫中導入數據。

為了連接 Mysql 數據庫,這里使用 MySqlConnector 驅動,請在創建控制臺項目之后,通過 nuget 引入此包。

MySqlConnector 的主要部件和 API 如下:

ADO.NET 類型 說明 異步方法 同步方法
DbConnection 連接器 OpenAsync Open
DbConnection BeginTransactionAsync BeginTransaction
DbCommand 執行命令 ExecuteNonQueryAsync ExecuteNonQuery
DbCommand ExecuteReaderAsync ExecuteReader
DbCommand ExecuteScalarAsync ExecuteScalar
DbDataReader 讀取數據 NextResultAsync NextResult
DbDataReader ReadAsync Read
DbTransaction 數據庫事務 CommitAsync Commit
DbTransaction RollbackAsync Rollback

使用同步方法可能會對托管線程池產生不利影響,如果沒有正確調優,還會導致速度減慢或鎖定。

Mysql 連接字符串配置示例:

const string connectionString = "Server=localhost;Port=3306;User ID=mysqltest;Password=Password123;Database=mysqldb";

或使用 MySqlConnectionStringBuilder 構建連接字符串:

var connectionBuilder = new MySqlConnectionStringBuilder()
	{
		Server = "localhost",
		Port = 3306,
		UserID = "mysqltest",
		Password = "Password123",
		Database = "mysqldb"
};
var connectionString = connectionBuilder.ConnectionString;

詳細連接字符串配置可以在 https://mysqlconnector.net/connection-options/ 中找到。

為了讓 MysqlConnetor 可以記錄日志,需要手動配置日志程序。

完整的 nuget 包如下:

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
    <PackageReference Include="MySqlConnector" Version="2.3.1" />
    <PackageReference Include="MySqlConnector.Logging.Microsoft.Extensions.Logging" Version="2.1.0" />
  </ItemGroup>

配置連接字符串、配置日志、創建數據庫連接,完整代碼示例如下:

var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
var logger = loggerFactory.CreateLogger<Program>();
var dataSourceBuilder = new MySqlDataSourceBuilder(connectionString);
dataSourceBuilder.UseLoggerFactory(loggerFactory);
await using var dataSource = dataSourceBuilder.Build();

using var connection = dataSource.CreateConnection();

經過以上配置之后,我們擁有了模擬數據庫以及基礎代碼,下面我們來正式學習 MysqlConnetor 和數據庫事務相關的知識。

Mysql 數據庫事務基礎

百度百科:數據庫事務( transaction)是訪問并可能操作各種數據項的一個數據庫操作序列,這些操作要么全部執行,要么全部不執行,是一個不可分割的工作單位。事務由事務開始與事務結束之間執行的全部數據庫操作組成。

數據庫事務有四個特性:

  • 原子性:原子性是指包含事務的操作要么全部執行成功,要么全部失敗回滾。
  • 一致性:一致性指事務在執行前后狀態是一致的。
  • 隔離性:一個事務所進行的修改在最終提交之前,對其他事務是不可見的。
  • 持久性:數據一旦提交,其所作的修改將永久地保存到數據庫中。

相信大家對數據庫事務都不陌生,因此這里就不扯淡了,下面來講解不同數據庫事務的特征。

數據庫的并發一致性問題

雖然數據庫事務可以幫助我們執行數據庫操作、回滾操作,但是數據庫事務并發執行時,事務之間可能會相互干擾,比如臟讀、幻讀等現象,我們使用數據庫事務時,要根據嚴格程度和性能之間相互平衡選擇事務隔離級別。

當多個事務并發執行時,可能會出現以下問題:

臟讀

? 事務 A 更新了數據,但還沒有提交,這時事務 B 讀取到事務 A 更新后的數據,然后事務 A 回滾了,事務 B 讀取到的數據就成為臟數據了。

不可重復讀

? 事務 A 對數據進行多次讀取,事務 B 在事務 A 多次讀取的過程中執行了更新操作并提交了,導致事務 A 多次讀取到的數據并不一致。

不可重復讀,特征是相同的數據,在事務 A 的不同階段讀取的數據不一樣。

幻讀

? 事務 A 在讀取數據后,事務 B 向事務A讀取的數據中插入了幾條數據,事務 A 再次讀取數據時發現多了幾條數據,和之前讀取的數據不一致。

幻讀,前后數據量不一樣。

丟失修改

? 事務 A 和事務 B 都對同一個數據進行修改,事務 A 先修改,事務 B 隨后修改,事務 B 的修改覆蓋了事務 A 的修改。

不可重復度和幻讀看起來比較像,它們主要的區別是:在不可重復讀中,發現數據不一致主要是數據被更新了。在幻讀中,發現數據不一致主要是數據增多或者減少了。

數據庫事務的隔離級別

數據庫事務的隔離級別有以下四種,按隔離級別從低到高:

  • 未提交讀:一個事務在提交前,它的修改對其他事務也是可見的。
  • 提交讀:一個事務提交之后,它的修改才能被其他事務看到。
  • 可重復讀:在同一個事務中多次讀取到的數據是一致的。
  • 串行化:需要加鎖實現,會強制事務串行執行。

Ado.net 中使用 System.Data.IsolationLevel 枚舉表示以上幾種數據庫事務隔離級別:

	public enum IsolationLevel
	{
        // 未指定
		Unspecified = -1,
        // 不能覆蓋來自更高度隔離的事務的掛起的更改。
		Chaos = 16,
        // 未提交讀,臟讀是可能的,這意味著不會發出共享鎖,也不會使用獨占鎖。
		ReadUncommitted = 256,
        // 提交讀,在讀取數據時持有共享鎖,以避免臟讀,但是數據可以在事務結束之前更改,從而導致不可重復讀取或幻像數據。
		ReadCommitted = 4096,
        // 可重復讀,鎖被放置在查詢中使用的所有數據上,防止其他用戶更新數據。防止不可重復讀取,但仍然可以使用幻像行。
		RepeatableRead = 65536,
        // 串行化,將在 DataSet 上放置一個范圍鎖,以防止其他用戶在事務完成之前更新數據集或將行插入數據集。
		Serializable = 1048576,
        // 通過存儲一個應用程序可以讀取而另一個應用程序正在修改相同數據的數據版本來減少阻塞。
        // 指示即使重新查詢,也無法從一個事務中看到在其他事務中所做的更改。
		Snapshot = 16777216
	}

數據庫的隔離級別分別可以解決數據庫的臟讀、不可重復讀、幻讀等問題。

隔離級別 臟讀 不可重復讀 幻讀
未提交讀 允許 允許 允許
提交讀 不允許 允許 允許
可重復讀 不允許 不允許 允許
串行化 不允許 不允許 不允許

其實也不必糾結這些問題,可以按照讀寫鎖的情況來理解。

編程中由于多個線程并發操作兩個字典:

Dictionary<string, string> a;
Dictionary<string, string> b;

第一個問題時,并發操作一個字典時,會出現線程并發異常。

所以,我們想要使用并發字典:

	ConcurrentDictionary<string, string> a;
	ConcurrentDictionary<string, string> b;

可是,當 T1 線程修改 a 完成,接著修改 b 時,線程 T2 把字典 a 修改了。這就導致了數據不一致。

使用讀寫鎖優化,將 a、b 兩個數據包在一起:

	ConcurrentDictionary<string, string> a;
	ConcurrentDictionary<string, string> b;

	private static ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();
	// 讀
	private void Read()
	{
		try
		{
			_lock.EnterReadLock(); 
			// 讀
		}
		catch { }
		finally
		{
			_lock.ExitReadLock();            // 釋放讀取鎖
		}
	}

	// 寫
	public void Write(int key, int value)
	{
		try
		{
			_lock.EnterUpgradeableReadLock();
			_lock.EnterWriteLock();
			// 寫
			_lock.ExitWriteLock();
		}
		catch { }
		finally
		{
			_lock.ExitUpgradeableReadLock();
		}
	}

讀寫鎖的原理很簡單,讀和寫是兩個沖突的操作。當沒有線程 時,多個線程可以并發 ,此時不會有任何問題。當有一個線程 時,既不允許有其它線程同時在 ,也不允許其它線程同時在 。也就是說, 是可以并發的,但是寫是獨占的。

串行化

當然對于數據庫事務就復雜了很多。如果要按照讀寫鎖的形式去做,那么其隔離級別相當于 串行化,整個表都被鎖住,不允許事務并發執行,此時不會有 臟讀不可重復讀幻讀 這些情況。

可是,這樣對于數據庫來說壓力是很大的,會嚴重拖垮數據庫的性能,以及嚴重降低了業務程序的并發量。

當事務 A 只需要修改 id=1,2,3 的數據時,使用 串行化 級別,會鎖住整個表。這樣似乎有點太浪費了。

可重復讀

那么,我們只需要鎖住事務 A 正在修改的那幾行記錄不就行了嗎?那么我們把數據庫事務下降一個級別,使用 可重復讀

使用 可重復讀 事務級別,其被鎖住的數據,依然保持安全,也就是不會被其它事務所修改。所以,不會出現 臟讀不可重復讀。但是因為不是鎖住整個表,因此其它事務是可以插入數據的,這就導致了會出現 幻讀。當然,可重復讀 出現的問題,一般來說只需要保證事務中只處理自己想要的數據即可。

可重復讀 導致的 幻讀 問題,比如 A 事務在 筆記本 分類下給聯想筆記本型號都打 9 折優惠,可是此時 B 事務從 筆記本 分類下,增加了幾個理想筆記本型號。結果,事務 A 最后一查詢,把 B 事務插入的數據查詢出來了。那么事務 A 查詢的數據就包含了打折和未打折的數據了。

InnoDB 使用 MVCC 來實現高并發性,并實現了所有 4 個SQL標準隔離級別。InnoDB 默認為 REPEATABLE READ (可重復讀)隔離級別,并且通過間隙鎖(next-key locking)策略來防止在這個隔離級別上的幻讀。InnoDB 不只鎖定在查詢中涉及的行,還會對索引結構中的間隙進行鎖定,以防止幻行被插入。

提交讀

使用示例:

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE  pet SET NAME = 'A';
SELECT SLEEP(5);
SELECT * from pet;
COMMIT;

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
UPDATE  pet SET NAME = 'B';
SELECT SLEEP(5);
SELECT * from pet;
COMMIT;

A 事務和 B 事務運行時,大家都對 name 做了修改,但是事務只能看到自己做出的修改,也就是說,B 事務未提交之前,A、B 都修改了數據,但是是隔離的。

A 事務修改了 name = A ,B 事務修改了 name = B ,未提交之前,A、B 事務讀到的分別是 A、B,這沒問題,不會干擾。

但是如果 A 先提交了事務,那么數據庫的 name 值就為 A,此時 B 事務還沒有提交,B 查詢到的 name = A,這就是不可重復讀。

提交讀 只能保證事務未提交前的數據隔離。當另一個事務提交后,會導致當前事務看到的數據前后不一樣。

未提交讀

這就離譜了。啥也不能保證。

對于數據庫事務的理解,大家倒序建議就比較容易理解了。

BeginTransaction() 和 TransactionScope 的區別

在 C# Ado.net 中,主要有兩種事務使用方式:

// 方式 1:
using var tran = await connection.BeginTransactionAsync();

// 方式 2:
using (TransactionScope transactionScope = new TransactionScope())
{

}

BeginTransaction() 由 IDbConnection 連接對象開啟,只能作用于當前 IDbConnection 。通過調用數據庫連接對象的 BeginTransaction() 方法,顯式地啟動了一個數據庫事務,因此與同步方法異步方法不沖突。

TransactionScope 內部封裝了一些 API,在TransactionScope設置的范圍內,不需要顯式地調用 Commit()Rollback() 方法,可以跨 IDbConnection 使用,在異步方法下使用需要做額外配置。

主要區別在于 BeginTransaction() 是顯式地管理事務,而 TransactionScope 則是在編程模型上提供了更為方便的自動事務管理機制。

在 System.Transactions 命名空間中存在很多與事務相關的代碼封裝。讀者可以自行了解:

https://learn.microsoft.com/en-us/dotnet/api/system.transactions?view=net-8.0

下面來詳細說明兩種事務開啟方式的使用區別。

BeginTransaction()

先說 BeginTransaction() ,其返回的是 DbTransaction 類型。

BeginTransaction() 開啟事務比較簡單,不過需要手動給 IDbCommand 設置事務屬性。

			await connection.OpenAsync();
           // 先開啟事務,再創建命令
			using var tran = await connection.BeginTransactionAsync();
			using var command = new MySqlCommand()
			{
				Connection = connection,
                // 注意這里
				Transaction = tran
			};

			try
			{
				command.CommandText = "... ...";
				await command.ExecuteNonQueryAsync();

				if(...)
				{
					await tran.CommitAsync();
				}else
				{
					await tran.RollbackAsync();
				}
			}
			catch (Exception ex)
			{
				await tran.RollbackAsync();
                logger.LogError(ex, "Tran error");
			}

BeginTransaction() 定義如下:

ValueTask<MySqlTransaction> BeginTransactionAsync(IsolationLevel isolationLevel, 
                                                  CancellationToken cancellationToken = default)

DbTransaction 還可以設置保存點。

			using var tran = await connection.BeginTransactionAsync();
			try
			{
				command.CommandText = "... ...";
				await command.ExecuteNonQueryAsync();

				// 保存點
				await tran.SaveAsync("stepa");

				// 釋放保存點、回滾到該保存點
				if(...)
				{
					await tran.ReleaseAsync("stepa");
				}
			}

BeginTransaction() 的使用比較簡單,也不太容易出錯。

可以不手動撤銷

很多時候我們會在 catch{} 回滾事務,如下代碼所示。

			try
			{
                ... ...
				await tran.CommitAsync();
			}
			catch (Exception ex)
			{
				logger.LogError(ex, "Tran error");
				await tran.RollbackAsync();
			}

實際上是當一個事務在 IDbConnection 中或者在此 IDbCommand 中沒有主動提交時,當對象生命周期結束或主動斷開連接時、被回收到連接池時,事務會自動回滾。只要沒有主動提交,則之前的操作皆無效。

比如,我們執行下面的 SQL 時,posts 表會被插入一條新的數據,id 為 101。

-- 開啟事務
BEGIN; -- 或者使用 START TRANSACTION;
INSERT INTO demo.posts (id, author_id, title, description, content, date)
VALUES (101, 1, '測試', '測試', '測試', '2023-12-08');
COMMIT ;

而執行以下代碼時,因為沒有調用 CommitAsync() 方法提交事務,因此程序結束后,插入數據庫的數據并不會起效。

			using var connection = dataSource.CreateConnection();
			await connection.OpenAsync();
			using var tran = await connection.BeginTransactionAsync();
			using var command = new MySqlCommand()
			{
				Connection = connection,
				Transaction = tran
			};

			try
			{
				command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (102, 1, '測試', '測試', '測試', '2023-12-08');
				""";
				await command.ExecuteNonQueryAsync();
                // await tran.CommitAsync();
			}
			catch (Exception ex)
			{
				logger.LogError(ex, "Tran error");
			}

TransactionScope

如以下代碼所示,雖然代碼執行不會報錯,但是其不受事務所控制,也就是說,雖然沒有提交,但是數據庫實實在在的插入了一條新的數據。

這是因為事務完全沒有起效,因為只有在 TransactionScope 中打開的數據庫連接,才會起效

			using var connection = dataSource.CreateConnection();
			await connection.OpenAsync();

			using (TransactionScope transactionScope = new TransactionScope())
			{
				var command = connection.CreateCommand();
				try
				{
					command.CommandText = 
                        """
                        INSERT INTO demo.posts (id, author_id, title, description, content, date) 
                        VALUES (103, 1, '測試', '測試', '測試', '2023-12-08');
                        """;
					await command.ExecuteNonQueryAsync();
					//transactionScope.Complete();
				}
				catch (Exception ex)
				{
					logger.LogError(ex, "Tran error");
				}
			}

修正之后:

			using (TransactionScope transactionScope = new TransactionScope())
			{
				using var connection = dataSource.CreateConnection();
				await connection.OpenAsync();

				var command = connection.CreateCommand();
				try
				{
					command.CommandText = 
                        """
                        INSERT INTO demo.posts (id, author_id, title, description, content, date) 
                        VALUES (104, 1, '測試', '測試', '測試', '2023-12-08');
                        """;
					await command.ExecuteNonQueryAsync();
					//transactionScope.Complete();
				}
				catch (Exception ex)
				{
					logger.LogError(ex, "Tran error");
				}
			}

但是,上面的代碼還是會報錯。這是因為 TransactionScope 默認不支持異步方法,而該代碼使用了異步,導致釋放時沒有使用相同的線程。

System.InvalidOperationException:“A TransactionScope must be disposed on the same thread that it was created.”

當然,TransactionScope 是支持異步的,我們只需要啟用配置即可。

			using (TransactionScope transactionScope = 
			new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
			{
				using var connection = dataSource.CreateConnection();
				await connection.OpenAsync();

				var command = connection.CreateCommand();
				try
				{
					command.CommandText = 
                        """
                        INSERT INTO demo.posts (id, author_id, title, description, content, date) 
                        VALUES (104, 1, '測試', '測試', '測試', '2023-12-08');
                        """;
					await command.ExecuteNonQueryAsync();
					//transactionScope.Complete();
				}
				catch (Exception ex)
				{
					logger.LogError(ex, "Tran error");
				}
			}

如下代碼所示,當執行代碼之后,因為我們沒有主動提交事務,因此,數據庫中不會真的插入數據。

			using (TransactionScope transactionScope = 
                   // 使其支持異步
                   new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
			{
				using var connection = dataSource.CreateConnection();
				await connection.OpenAsync();

				var command = connection.CreateCommand();

				command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (105, 1, '測試', '測試', '測試', '2023-12-08');
				""";
                    
				await command.ExecuteNonQueryAsync();
				//transactionScope.Complete();
			}

有了經驗之后,我們發現,如果我們不調用 Complete() 方法,那么數據庫中不會真的插入數據。

可是問題來了,因為是在 TransactionScope 中創建 IDbConnection 并打開連接,也就是說 TransactionScope 作用域范圍大于 IDbConnection ,那么 IDbConnection 釋放之后,再提交 TransactionScope ,是否可以?

			using (TransactionScope transactionScope = new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
			{
				using (var connection = dataSource.CreateConnection())
				{
					await connection.OpenAsync();
					var command = connection.CreateCommand();
					command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (105, 1, '測試', '測試', '測試', '2023-12-08');
				""";
					await command.ExecuteNonQueryAsync();
				}

				transactionScope.Complete();
			}

答案是一切正常。

簡化代碼如下所示:

			using (TransactionScope transactionScope = ...)
			{
				using (var connection = dataSource.CreateConnection())
				{
					await connection.OpenAsync();
					await command.ExecuteNonQueryAsync();
				}

				transactionScope.Complete();
			}

雖然, IDbConnection 在 using 中,transactionScope.Complete() 在 using 之外,但是事務依然可以起效。如果調用 .Complete(),則事務提交。如果不調用 .Complete() 則事務不會提交。

回到本小節第一個代碼示例中,事務不起效的問題。我們已經知道了是因為 IDbConnection 沒有在 TransactionScope 內創建,所以導致事務不能作用。

但是,對于 ASP.NET Core 程序、Context 形式的 ORM、倉儲形式的 ORM 等,由于其封裝在上下文內,不太可能在開發者使用 TransactionScope 時,再手動打開 IDbConnection.Open() 。不過這些 ORM 框架大多數都做了封裝,而本文末尾也介紹了幾種封裝方式。

總結

通過 BeginTransaction() 創建的事務,不會因為異步等出現問題,因為其是明確在一個 IDbCommand 、IDbConnection 中起效。

			using var tran = await connection.BeginTransactionAsync();
			using var command = new MySqlCommand()
			{
				Connection = connection,
                // 注意這里
				Transaction = tran
			};

所以說,通過 .BeginTransactionAsync() 使用事務,是最簡單、最不容易出錯的,而且其明確在哪個 IDbCommand 中使用事情,出現問題時,排除起來也相對簡單。

而對于 TransactionScope 來說,筆者花費了比較多的篇幅去實驗和解釋,TransactionScope 是使用事務作用域實現隱式事務的,使用起來有一定難度,也容易出錯。

DML 是否可以使用事務

開始的時候,筆者并沒有想到這個事情,在跟同事偶然吹水時,提到了這個事情。

Mysql 的事務對刪除表、創建表這些 DML 命令,其事務是無效的,起效的是表數據相關的操作,即 insert、update、delete 語句。

如下 SQL 所示,雖然回滾了事務,但是最后還是創建了視圖。

-- 開啟事務
use  demo;
BEGIN;
create view v_posts AS  SELECT * FROM posts;
ROLLBACK;
-- COMMIT ;

順序多操作

先從 TransactionScope 說起,情況如下代碼所示:

TransactionScope 中包含、創建了兩個 IDbConnection ,并且兩個 IDbConnection 都插入了數據。

也就是說使用 TransactionScope 同時管理多個 IDbConnection 。

			using (TransactionScope transactionScope = new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
			{
				using (var connection = dataSource.CreateConnection())
				{
					await connection.OpenAsync();
					var command = connection.CreateCommand();
					command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (108, 1, '測試', '測試', '測試', '2023-12-08');
				""";
					await command.ExecuteNonQueryAsync();
				}

				using (var connection = dataSource.CreateConnection())
				{
					await connection.OpenAsync();
					var command = connection.CreateCommand();
					command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (109, 1, '測試', '測試', '測試', '2023-12-08');
				""";
					await command.ExecuteNonQueryAsync();
				}

				//transactionScope.Complete();
			}

這樣是可以的,TransactionScope 管理在期內的所有 IDbConnection,讓他們在當前的事務中保持一致。

但是 BeginTransaction() 是使用 IDbConnection.BeginTransaction() 創建的,不能跨 IDbConnection 使用。

比如,以下代碼會報錯:

			using var connection1 = dataSource.CreateConnection();
			using var connection2 = dataSource.CreateConnection();
			await connection1.OpenAsync();
			await connection2.OpenAsync();

			try
			{
				var tran1 = connection1.BeginTransaction();

				var command1 = connection1.CreateCommand();
				command1.Transaction = tran1;
				var command2 = connection2.CreateCommand();
				command2.Transaction = tran1;

				command1.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (108, 1, '測試', '測試', '測試', '2023-12-08');
				""";
				await command1.ExecuteNonQueryAsync();
				command2.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (108, 1, '測試', '測試', '測試', '2023-12-08');
				""";
				await command2.ExecuteNonQueryAsync();
				tran1.Commit();
			}
			catch (Exception ex)
			{
				logger.LogError(ex, "Tran error");
			}

所以,這里又有一個區別。

嵌套事務

.BeginTransaction() 不支持嵌套事務,代碼如下所示:

		static async Task Main(string[] args)
        {
            using var connection = dataSource.CreateConnection();
			await connection.OpenAsync();
			var tran = connection.BeginTransaction();

			try
			{
				var command = connection.CreateCommand();
				command.Transaction = tran;
				command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (110, 1, '測試', '測試', '測試', '2023-12-08');
				""";
				await command.ExecuteNonQueryAsync();

				// 嵌套事務
				try
				{
					await InsertAsync(connection);
				}
				catch (Exception ex)
				{
					logger.LogError(ex, "Tran error.");
					await tran.RollbackAsync();
					return;
				}

				await tran.RollbackAsync();
			}
			catch (Exception ex)
			{
				logger.LogError(ex, "Tran error");
			}
		}

		// 嵌套的子事務
		private static async Task InsertAsync(MySqlConnection connection)
		{
			var tran = connection.BeginTransaction();
			var command = connection.CreateCommand();
			command.Transaction = tran;
			command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (112, 1, '測試', '測試', '測試', '2023-12-08');
				""";
			await command.ExecuteNonQueryAsync();
			await tran.CommitAsync();
		}

當一個 IDbConnection 調用兩次 .BeginTransaction() 時,代碼會報錯。

 System.InvalidOperationException: Transactions may not be nested.

所以,我們只能寄望于 TransactionScope。

使用 TransactionScope 做嵌套事務,可以做到靈活的邏輯定制,每個嵌套子事務都有自己的邏輯。

每個子事務只需要正常編寫自己的 TransactionScope 即可,即使子事務的 TransactionScope 已提交,如果最外層的 TransactionScope 事務沒有提交,則所有的事務都不會提交。

如下代碼所示:

	static async Task Main(string[] args)
	{
		using var connection = dataSource.CreateConnection();
		using (TransactionScope transactionScope = new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
		{
			await connection.OpenAsync();
			var command = connection.CreateCommand();
			command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (110, 1, '測試', '測試', '測試', '2023-12-08');
				""";
			await command.ExecuteNonQueryAsync();

			// 嵌套事務
			try
			{
				await InsertAsync(connection);
			}
			catch (Exception ex)
			{
				logger.LogError(ex, "Tran error.");
				return;
			}
			// transactionScope.Complete();
		}
	}

	// 嵌套的子事務
	private static async Task InsertAsync(MySqlConnection connection)
	{
		using (TransactionScope transactionScope = new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
		{
			var command = connection.CreateCommand();
			command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (112, 1, '測試', '測試', '測試', '2023-12-08');
				""";
			await command.ExecuteNonQueryAsync();
			transactionScope.Complete();
		}
	}

雖然 InsertAsync() 中的事務已經提交,但是由于其受到外層 TransactionScope 事務的影響,因此當外層事務不提交時,子事務也不會提交。

當然,即使不是同一個 IDbConnection 也是可以的。

	static async Task Main(string[] args)
	{
		using var connection = dataSource.CreateConnection();
		using (TransactionScope transactionScope = new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
		{
			await connection.OpenAsync();
			var command = connection.CreateCommand();
			command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (110, 1, '測試', '測試', '測試', '2023-12-08');
				""";
			await command.ExecuteNonQueryAsync();

			// 嵌套事務
			try
			{
				await InsertAsync();
			}
			catch (Exception ex)
			{
				logger.LogError(ex, "Tran error.");
				return;
			}
			// transactionScope.Complete();
		}
	}

	// 嵌套的子事務
	private static async Task InsertAsync()
	{
		using var connection = dataSource.CreateConnection();
		using (TransactionScope transactionScope = new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
		{
			await connection.OpenAsync();
			var command = connection.CreateCommand();
			command.CommandText = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (112, 1, '測試', '測試', '測試', '2023-12-08');
				""";
			await command.ExecuteNonQueryAsync();
			transactionScope.Complete();
		}
	}

所以,每個方法的代碼,只需要關注自己的邏輯即可。對于模塊分離、職責分離的代碼很有用。

事務范圍

前面我們提到了 TransactionScope 的嵌套事務。

TransactionScope 對于嵌套事務的處理,有一個 TransactionScopeOption 枚舉配置。

	public enum TransactionScopeOption
	{
        // 該范圍需要一個事務。 如果已經存在環境事務,則使用該環境事務。 否則,在進入范圍之前創建新的事務。 這是默認值。
		Required = 0,
        
        // 總是為該范圍創建新事務。
		RequiresNew = 1,
        
        // 如果使用 Suppress 實例化范圍,則無論是否存在環境事務,該范圍都不會參與事務。使用此值實例化的范圍始終將 null 作為其環境事務。
		Suppress = 2
	}

使用示例:

using(TransactionScope scope1 = new TransactionScope())
{
    // 默認支持嵌套
    using(TransactionScope scope2 = new TransactionScope(TransactionScopeOption.Required))
    {
        //...
    }
    
    // 不受 scope1 的影響
    using(TransactionScope scope3 = new TransactionScope(TransactionScopeOption.RequiresNew))
    {
        //...  
    }
  
    // 如果使用 Suppress 實例化范圍,則無論是否存在環境事務,該范圍都不會參與事務。
    using(TransactionScope scope4 = new TransactionScope(TransactionScopeOption.Suppress))
    {
        //...  
    }
}

對于嵌套事務作用域范圍,讀者可以從這篇文章中了解更多:https://learn.microsoft.com/en-us/previous-versions/ms172152(v=vs.90)?redirectedfrom=MSDN#Y1642

封裝 DbContext

前面提到過,IDbConnection 需要在 TransactionScope 中打開連接,TransactionScope 才能管控其連接的事務。

不過,有一些數據庫驅動已經支持了 TransactionScope ,即使不在其內打開鏈接也可以。比如 EFCore 框架,EFCore 自動管理 IDbConnection 的生命周期,因此我們往往不會手動管理連接,因此事務事務時,我們不太可能這樣做:

MyContext _context;

using (TransactionScope transactionScope = ...)
{
    _context.Connection.Open()
}

在使用數據庫事務之前,往往連接早就已經打開了。

MyContext _context;
_context.SelectAsync()....
_context.User.SectAsync()....
using (TransactionScope transactionScope = ...)
{
}

所以,我們需要封裝一個上下文類型,能夠在連接打開后,自動使用上下文的事務。

TransactionScope

封裝一個數據庫上下文,執行命令時,如果發現其在事務范圍內,則主動使用上下文事務。

	public class DbContext
	{
		private readonly DbConnection _connection;

		public DbContext(DbConnection connection)
		{
			_connection = connection;
		}

		public async Task ExecuteAsync(string sql)
		{
			var command = _connection.CreateCommand();
            // 獲取當前事務
			var tran = Transaction.Current;
			if (tran != null)
			{
                // 注意這里。
				_connection.EnlistTransaction(tran);
			}

			command.CommandText = sql;
            
			await command.ExecuteNonQueryAsync();
		}
	}

使用示例:

		using var connection = dataSource.CreateConnection();
// 在之外打開
		await connection.OpenAsync();
		var context = new DbContext(connection);

		using (TransactionScope transactionScope = new TransactionScope(asyncFlowOption: TransactionScopeAsyncFlowOption.Enabled))
		{
			var sql = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (111, 1, '測試', '測試', '測試', '2023-12-08');
				""";

			await context.ExecuteAsync(sql);
		}

BeginTransaction()

使用上下文的形式封裝 BeginTransaction() 開啟的事務比較簡單,只需要手動維護 DbTransaction 即可。


	public class DbContext
	{
		private readonly DbConnection _connection;
		private DbTransaction? _tran;
		public DbContext(MySqlConnection connection)
		{
			_connection = connection;
		}

		public async Task OpenTran()
		{
			if (_tran != null) throw new Exception("請勿重復開啟事務");
			_tran = await _connection.BeginTransactionAsync();
		}

		public async Task ExecuteAsync(string sql)
		{
			var command = _connection.CreateCommand();
			command.CommandText = sql;

			if (_tran != null)
			{
				command.Transaction = _tran;
			}
			await command.ExecuteNonQueryAsync();
		}

		public async Task EndTran()
		{
			if (_tran == null) throw new Exception("未開啟事務");
			await _tran.CommitAsync();
			_tran.Dispose();
			_tran = null;
		}
	}

使用方法:

		using var connection = dataSource.CreateConnection();
		await connection.OpenAsync();
		DbContext context = new DbContext(connection);

		await context.OpenTran();
		var sql = """
				INSERT INTO demo.posts (id, author_id, title, description, content, date) 
				VALUES (111, 1, '測試', '測試', '測試', '2023-12-08');
				""";
		await context.ExecuteAsync(sql);

當然,由于不同的 ORM 封裝的數據庫事務方法不一樣,因此 ORM 的差異比較大。

總結

以上是生活随笔為你收集整理的C# 从代码入门 Mysql 数据库事务的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。