EF Core 中使用事務

一个人走在路上發表於2024-03-30

使用事務
專案
2023/10/05
14 個參與者

反饋
本文內容
預設事務行為
控制事務
儲存點
跨上下文事務
使用外部 DbTransactions(僅限關聯式資料庫)
使用 System.Transactions
顯示較少選項
事務允許以原子方式處理多個資料庫操作。 如果已提交事務,則所有操作都會成功應用到資料庫。 如果已回滾事務,則所有操作都不會應用到資料庫。

提示

可在 GitHub 上檢視此文章的示例。

預設事務行為
預設情況下,如果資料庫提供程式支援事務,則會在事務中應用對 SaveChanges 的單一呼叫中的所有更改。 如果其中有任何更改失敗,則會回滾事務且所有更改都不會應用到資料庫。 這意味著,SaveChanges 可保證完全成功,或在出現錯誤時不修改資料庫。

對於大多數應用程式,此預設行為已足夠。 如果應用程式要求被視為有必要,則應該僅手動控制事務。

控制事務
可以使用 DbContext.Database API 開始、提交和回滾事務。 以下示例顯示了在單個事務中執行的兩個 SaveChanges 操作以及一個 LINQ 查詢:

C#

複製
using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

try
{
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context.SaveChanges();

context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/visualstudio" });
context.SaveChanges();

var blogs = context.Blogs
.OrderBy(b => b.Url)
.ToList();

// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
transaction.Commit();
}
catch (Exception)
{
// TODO: Handle failure
}
雖然所有關聯式資料庫提供程式都支援事務,但在呼叫事務 API 時,可能會引發其他提供程式型別或不執行任何操作。

備註

以這種方式手動控制事務的操作與隱式呼叫的重試執行策略不相容。 有關詳細資訊,請參閱連線復原能力。

儲存點
如果呼叫 SaveChanges 且事務已在上下文中進行,則在儲存任何資料之前,EF 會自動建立儲存點。 儲存點是資料庫事務中的點,如果發生錯誤或出於任何其他原因,可能會回滾到這些點。 如果 SaveChanges 遇到任何錯誤,則會自動將事務回滾到儲存點,使事務處於相同狀態,就好像從未開始。 這樣可以解決問題並重試儲存,尤其是在出現樂觀併發問題時。

警告

儲存點與 SQL Server 的多重活動結果集 (MARS) 不相容。 當連線上啟用了 MARS 時,EF 不會建立儲存點,即使 MARS 未處於活動使用狀態。 如果在 SaveChanges 過程中出現錯誤,則該事務可能處於未知狀態。

還可以手動管理儲存點,就像管理事務一樣。 以下示例在事務中建立儲存點,並在失敗時回滾到該儲存點:

C#

複製
using var context = new BloggingContext();
using var transaction = context.Database.BeginTransaction();

try
{
context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/dotnet/" });
context.SaveChanges();

transaction.CreateSavepoint("BeforeMoreBlogs");

context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/visualstudio/" });
context.Blogs.Add(new Blog { Url = "https://devblogs.microsoft.com/aspnet/" });
context.SaveChanges();

transaction.Commit();
}
catch (Exception)
{
// If a failure occurred, we rollback to the savepoint and can continue the transaction
transaction.RollbackToSavepoint("BeforeMoreBlogs");

// TODO: Handle failure, possibly retry inserting blogs
}
跨上下文事務
您還可以跨多個上下文例項共享一個事務。 此功能僅在使用關聯式資料庫提供程式時才可用,因為它需要使用特定於關聯式資料庫的 DbTransaction 和 DbConnection。

若要共享事務,上下文必須共享 DbConnection 和 DbTransaction。

允許在外部提供連線
共享 DbConnection 需要在構造上下文時向其中傳入連線的功能。

允許在外部提供 DbConnection 的最簡單方式是,停止使用 DbContext.OnConfiguring 方法來配置上下文並在外部建立 DbContextOptions,然後將其傳遞到上下文建構函式。

提示

DbContextOptionsBuilder 是在 DbContext.OnConfiguring 中用於配置上下文的 API,現在即將在外部使用它來建立 DbContextOptions。

C#

複製
public class BloggingContext : DbContext
{
public BloggingContext(DbContextOptions<BloggingContext> options)
: base(options)
{
}

public DbSet<Blog> Blogs { get; set; }
}
替代方法是繼續使用 DbContext.OnConfiguring,但接受已儲存並隨後在 DbContext.OnConfiguring 中使用的 DbConnection。

C#

複製
public class BloggingContext : DbContext
{
private DbConnection _connection;

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

public DbSet<Blog> Blogs { get; set; }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connection);
}
}
共享連線和事務
現在可以建立共享同一連線的多個上下文例項。 然後使用 DbContext.Database.UseTransaction(DbTransaction) API 在同一事務中登記兩個上下文。

C#

複製
using var connection = new SqlConnection(connectionString);
var options = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(connection)
.Options;

using var context1 = new BloggingContext(options);
using var transaction = context1.Database.BeginTransaction();
try
{
context1.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context1.SaveChanges();

using (var context2 = new BloggingContext(options))
{
context2.Database.UseTransaction(transaction.GetDbTransaction());

var blogs = context2.Blogs
.OrderBy(b => b.Url)
.ToList();

context2.Blogs.Add(new Blog { Url = "http://dot.net" });
context2.SaveChanges();
}

// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
transaction.Commit();
}
catch (Exception)
{
// TODO: Handle failure
}
使用外部 DbTransactions(僅限關聯式資料庫)
如果使用多個資料訪問技術來訪問關聯式資料庫,則可能希望在這些不同技術所執行的操作之間共享事務。

以下示例顯示瞭如何在同一事務中執行 ADO.NET SqlClient 操作和 Entity Framework Core 操作。

C#

複製
using var connection = new SqlConnection(connectionString);
connection.Open();

using var transaction = connection.BeginTransaction();
try
{
// Run raw ADO.NET command in the transaction
var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandText = "DELETE FROM dbo.Blogs";
command.ExecuteNonQuery();

// Run an EF Core command in the transaction
var options = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(connection)
.Options;

using (var context = new BloggingContext(options))
{
context.Database.UseTransaction(transaction);
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context.SaveChanges();
}

// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
transaction.Commit();
}
catch (Exception)
{
// TODO: Handle failure
}
使用 System.Transactions
如果需要跨較大作用域進行協調,則可以使用環境事務。

C#

複製
using (var scope = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
using var connection = new SqlConnection(connectionString);
connection.Open();

try
{
// Run raw ADO.NET command in the transaction
var command = connection.CreateCommand();
command.CommandText = "DELETE FROM dbo.Blogs";
command.ExecuteNonQuery();

// Run an EF Core command in the transaction
var options = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(connection)
.Options;

using (var context = new BloggingContext(options))
{
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context.SaveChanges();
}

// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
scope.Complete();
}
catch (Exception)
{
// TODO: Handle failure
}
}
還可以在顯式事務中登記。

C#

複製
using (var transaction = new CommittableTransaction(
new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }))
{
var connection = new SqlConnection(connectionString);

try
{
var options = new DbContextOptionsBuilder<BloggingContext>()
.UseSqlServer(connection)
.Options;

using (var context = new BloggingContext(options))
{
context.Database.OpenConnection();
context.Database.EnlistTransaction(transaction);

// Run raw ADO.NET command in the transaction
var command = connection.CreateCommand();
command.CommandText = "DELETE FROM dbo.Blogs";
command.ExecuteNonQuery();

// Run an EF Core command in the transaction
context.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/dotnet" });
context.SaveChanges();
context.Database.CloseConnection();
}

// Commit transaction if all commands succeed, transaction will auto-rollback
// when disposed if either commands fails
transaction.Commit();
}
catch (Exception)
{
// TODO: Handle failure
}
}
備註

如果使用非同步 API,請確保在 TransactionScope 建構函式中指定 TransactionScopeAsyncFlowOption.Enabled ,以確保環境事務跨非同步呼叫流動。

有關 TransactionScope 和環境事務的詳細資訊,請參閱此文件。

System.Transactions 的限制
EF Core 依賴資料庫提供程式以實現對 System.Transactions 的支援。 如果提供程式未實現對 System.Transactions 的支援,則可能會完全忽略對這些 API 的呼叫。 SqlClient 支援它。

重要

建議你測試在依賴提供程式以管理事務之前 API 與該提供程式的行為是否正確。 如果不正確,則建議你與資料庫提供程式的維護人員聯絡。

System.Transactions 中的分散式事務支援僅新增到了 Windows 版 .NET 7.0。 嘗試在較舊的 .NET 版本或非 Windows 平臺上使用分散式事務將失敗。

TransactionScope 不支援非同步提交/回滾;這意味著同步處置它會阻塞正在執行的執行緒,直到該操作完成。

相關文章