.net 如何在EF Core 6中嵌套交易?

ifsvaxew  于 2023-06-25  发布在  .NET
关注(0)|答案(4)|浏览(111)

在某些情况下,我已经在我的存储库函数中使用事务,因为我有时需要同时向两个表中插入数据,如果其中一个插入失败,我希望整个操作失败。
现在我遇到了一种情况,我必须在另一个事务中 Package 对多个存储库/函数的调用,但是当其中一个函数已经在内部使用事务时,我将得到错误The connection is already in a transaction and cannot participate in another transaction
我不想从存储库函数中删除事务,因为这意味着我必须知道哪些存储库函数需要事务,然后我必须在服务层中实现事务。另一方面,当存储库函数已经在内部使用事务时,我似乎不能在事务中使用它们。下面是一个例子,说明我在哪里遇到了这个问题:

// Reverse engineered classes
public partial class TblProject
{
    public TblProject()
    {
        TblProjectStepSequences = new HashSet<TblProjectStepSequence>();
    }
    
    public int ProjectId { get; set; }

    public virtual ICollection<TblProjectStepSequence> TblProjectStepSequences { get; set; }
}

public partial class TblProjectTranslation
{
    public int ProjectId { get; set; }
    public string Language { get; set; }
    public string ProjectName { get; set; }

    public virtual TblProject Project { get; set; }
}

public partial class TblProjectStepSequence
{
    public int SequenceId { get; set; }
    public int ProjectId { get; set; }
    public int StepId { get; set; }
    public int SequencePosition { get; set; }

    public virtual TblStep Step { get; set; }
    public virtual TblProject Project { get; set; }
}

// Creating a project in the ProjectRepository
public async Task<int> CreateProjectAsync(TblProject project, ...)
{
    using (var transaction = this.Context.Database.BeginTransaction())
    {
        await this.Context.TblProjects.AddAsync(project);
        await this.Context.SaveChangesAsync();
        // Insert translations... (project Id is required for this)
        await this.Context.SaveChangesAsync();
        transaction.Commit();
        
        return entity.ProjectId;
    }
}

// Creating the steps for a project in the StepRepository
public async Task<IEnumerable<int>> CreateProjectStepsAsync(int projectId, IEnumerable<TblProjectStepSequence> steps)
{
    await this.Context.TblProjectStepSequences.AddRangeAsync(steps);
    await this.Context.SaveChangesAsync();

    return steps.Select(step =>
    {
        return step.SequenceId;
    }
    );
}

// Creating a project with its steps in the service layer
public async Task<int> CreateProjectWithStepsAsync(TblProject project, IEnumerable<TblProjectStepSequence> steps)
{
    // This is basically a wrapper around Database.BeginTransaction() and IDbContextTransaction
    using (Transaction transaction = await transactionService.BeginTransactionAsync())
    {
        int projectId = await projectRepository.CreateProjectAsync(project);
        await stepRepository.CreateProjectStepsAsync(projectId, steps);

        return projectId;
    }
}

有没有一种方法可以让多个事务嵌套在彼此内部,而不知道内部事务中可能有一个外部事务?
我知道从技术的Angular 来看,实际上可能不可能嵌套这些事务,但我仍然需要一个解决方案,该解决方案使用存储库的内部事务或外部事务(如果存在的话),因此我不可能不小心忘记将事务用于需要事务的存储库功能。

s71maibg

s71maibg1#

您可以检查CurrentTransaction属性并执行以下操作:

var transaction = Database.CurrentTransaction ?? Database.BeginTransaction()

如果已经有一个事务使用它,否则开始一个新的事务...
编辑:删除了Using块,请参阅注解。提交/回滚事务需要更多的逻辑...

oaxa6hgo

oaxa6hgo2#

我正在回答你问的问题“如何在EF Core 6中嵌套交易?”“

  • 请注意,这只是一个直接的答案,而不是评估什么是最佳实践,什么不是。围绕最佳实践进行了大量讨论,这对于质疑什么最适合您的用例是有效的,但不是问题的答案(请记住,Stack overflow只是一个Q+A网站,人们希望得到直接的答案)。

说了这么多,我们继续主题:
尝试使用这个helper函数来创建一个新的事务:

public CommittableTransaction CreateTransaction() 
    => new System.Transactions.CommittableTransaction(new TransactionOptions()
    {
        IsolationLevel = System.Transactions.IsolationLevel.ReadCommitted
    });

使用Northwind数据库作为示例数据库,您可以像这样使用它:

public async Task<int?> CreateCategoryAsync(Categories category)
{         
    if (category?.CategoryName == null) return null;
    using(var trans = CreateTransaction()) 
    {
        await this.Context.Categories.AddAsync(category);
        await this.Context.SaveChangesAsync();

        trans.Commit();

        return category?.CategoryID;
    }
}

然后你可以从另一个函数调用它,比如:

/// <summary>Create or use existing category with associated products</summary>
/// <returns>Returns null if transaction was rolled back, else CategoryID</returns>
public async Task<int?> CreateProjectWithStepsAsync(Categories category)
{
    using var trans = CreateTransaction();
    
    int? catId = GetCategoryId(category.CategoryName) 
                ?? await CreateCategoryAsync(category);

    if (!catId.HasValue || string.IsNullOrWhiteSpace(category.CategoryName))
    {
        trans.Rollback(); return null;
    }
    
    var product1 = new Products()
    {
        ProductName = "Product A1", CategoryID = catId
    };
    await this.Context.Products.AddAsync(product1);
    
    var product2 = new Products()
    {
        ProductName = "Product A2", CategoryID = catId
    };
    await this.Context.Products.AddAsync(product2);

    await this.Context.SaveChangesAsync();

    trans.Commit();
    
    return catId;
}

要使用LinqPad运行这个,你需要一个入口点(当然,通过F4添加NUGET包EntityFramework 6.x,然后创建一个EntityFramework Core连接):

// Main method required for LinqPad
UserQuery Context;

async Task Main()
{
    Context = this;
    var category = new Categories()
    {
        CategoryName = "Category A1"
        // CategoryName = ""
    };
    var catId = await CreateProjectWithStepsAsync(category);
    Console.WriteLine((catId == null) 
           ? "Transaction was aborted." 
           : "Transaction successful.");
}

这只是一个简单的示例-它不会检查是否存在同名的任何产品,它只会创建一个新产品。你可以很容易地实现它,我已经在类别的函数CreateProjectWithStepsAsync中展示了它:

int? catId = GetCategoryId(category.CategoryName) 
            ?? await CreateCategoryAsync(category);

首先,它按名称查询类别(通过GetCategoryId(...)),如果结果是null,它将创建一个新类别(通过CreateCategoryAsync(...))。
另外,你需要考虑isolation level:查看System.Transactions.IsolationLevel,看看这里使用的(ReadCommitted)是否适合您(这是默认设置)。
它所做的是显式地创建一个事务,注意这里我们有一个事务中的事务。

注:

  • using的两种方式我都用过-旧的和新的。挑一个你更喜欢的。
fzwojiic

fzwojiic3#

我就像这样...

public class MyContext : DbContext
{
    // ....

    public async Task UsingTransaction(Func<Task> asyncAction)
    {
        if (Database.CurrentTransaction != null)
        {
            await asyncAction();
        }
        else
        {
            using (var transaction = Database.BeginTransaction())
            {
                try
                {
                    await asyncAction();
                    transaction.Commit();
                }
                catch
                {
                    transaction.Rollback();
                    throw;
                }
            }
        }
    }
}

所以我用这样的代码调用它

await _context.UsingTransaction(async () => {

    // ...
    await _context.SaveChangesAsync();

    // ...
    await _context.SaveChangesAsync();

});

ps.建议您也为同步代码创建一个重载...

public void UsingTransaction(Action action)
{
   //... sync code
}
pbpqsu0x

pbpqsu0x4#

只是 * 不要 * 多次调用SaveChanges
问题是由于多次调用SaveChanges提交对DbContext所做的更改,而不是在最后只调用一次。根本不需要。DbContext是一个多实体工作单元。它甚至不保持与数据库的开放连接。通过消除交叉连接阻塞,整个应用程序的吞吐量提高了100 - 1000倍。
DbContext跟踪对它跟踪的对象所做的所有修改,并在使用内部事务调用SaveChanges时持久化/提交它们。要放弃更改,只需释放DbContext。这就是为什么所有示例都显示在using块中使用DbContext的原因-这实际上是工作单元“事务”的范围。
不需要先“保存”父对象。EF Core将在SaveChanges内部处理此问题。
使用EF Core documentation tutorial中的Blog/Posts示例:

public class BloggingContext : DbContext
{
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    public string DbPath { get; }

    // The following configures EF to create a Sqlite database file in the
    // special "local" folder for your platform.
    protected override void OnConfiguring(DbContextOptionsBuilder options)
        => options.UseSqlServer($"Data Source=.;Initial Catalog=tests;Trusted_Connection=True; Trust Server Certificate=Yes");
}

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }

    public List<Post> Posts { get; } = new();
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public int BlogId { get; set; }
    public Blog Blog { get; set; }
}

下面的Program.cs将添加一个包含5篇文章的博客,但最后只调用SaveChanges一次:

using (var db = new BloggingContext())
{
    Blog blog = new Blog { Url = "http://blogs.msdn.com/adonet" };
    IEnumerable<Post> posts = Enumerable.Range(0, 5)
                                       .Select(i => new Post {
                                            Title = $"Hello World {i}",
                                            Content = "I wrote an app using EF Core!"
                                         });
    blog.Posts.AddRange(posts);

    db.Blogs.Add(blog);

    await db.SaveChangesAsync();
}

代码从不指定或检索ID。Add是一个内存操作,所以没有理由使用AddAsyncAddInserted状态下开始跟踪blog和相关的Post s。
其后各表的内容为:

select * from blogs

select * from posts;
-----------------------

BlogId  Url
1   http://blogs.msdn.com/adonet

PostId  Title   Content BlogId
1   Hello World 0   I wrote an app using EF Core!   1
2   Hello World 1   I wrote an app using EF Core!   1
3   Hello World 2   I wrote an app using EF Core!   1
4   Hello World 3   I wrote an app using EF Core!   1
5   Hello World 4   I wrote an app using EF Core!   1

执行两次代码将添加另一个博客,其中包含另外5篇文章。

PostId  Title   Content BlogId
1   Hello World 0   I wrote an app using EF Core!   1
2   Hello World 1   I wrote an app using EF Core!   1
3   Hello World 2   I wrote an app using EF Core!   1
4   Hello World 3   I wrote an app using EF Core!   1
5   Hello World 4   I wrote an app using EF Core!   1
6   Hello World 0   I wrote an app using EF Core!   2
7   Hello World 1   I wrote an app using EF Core!   2
8   Hello World 2   I wrote an app using EF Core!   2
9   Hello World 3   I wrote an app using EF Core!   2
10  Hello World 4   I wrote an app using EF Core!   2

使用SQL Server XEvents事件探查器显示进行了以下SQL调用:

exec sp_executesql N'SET NOCOUNT ON;
INSERT INTO [Blogs] ([Url])
VALUES (@p0);
SELECT [BlogId]
FROM [Blogs]
WHERE @@ROWCOUNT = 1 AND [BlogId] = scope_identity();
',N'@p0 nvarchar(4000)',@p0=N'http://blogs.msdn.com/adonet'

exec sp_executesql N'SET NOCOUNT ON;
DECLARE @inserted0 TABLE ([PostId] int, [_Position] [int]);
MERGE [Posts] USING (
VALUES (@p1, @p2, @p3, 0),
(@p4, @p5, @p6, 1),
(@p7, @p8, @p9, 2),
(@p10, @p11, @p12, 3),
(@p13, @p14, @p15, 4)) AS i ([BlogId], [Content], [Title], _Position) ON 1=0
WHEN NOT MATCHED THEN
INSERT ([BlogId], [Content], [Title])
VALUES (i.[BlogId], i.[Content], i.[Title])
OUTPUT INSERTED.[PostId], i._Position
INTO @inserted0;
SELECT [i].[PostId] FROM @inserted0 i
ORDER BY [i].[_Position];
',N'@p1 int,@p2 nvarchar(4000),@p3 nvarchar(4000),@p4 int,@p5 nvarchar(4000),@p6 nvarchar(4000),@p7 int,@p8 nvarchar(4000),@p9 nvarchar(4000),@p10 int,@p11 nvarchar(4000),@p12 nvarchar(4000),@p13 int,@p14 nvarchar(4000),@p15 nvarchar(4000)',@p1=3,@p2=N'I wrote an app using EF Core!',@p3=N'Hello World 0',@p4=3,@p5=N'I wrote an app using EF Core!',@p6=N'Hello World 1',@p7=3,@p8=N'I wrote an app using EF Core!',@p9=N'Hello World 2',@p10=3,@p11=N'I wrote an app using EF Core!',@p12=N'Hello World 3',@p13=3,@p14=N'I wrote an app using EF Core!',@p15=N'Hello World 4'

不寻常的SELECT和MERGE用于确保IDENTITY值以插入对象的顺序返回,因此EF Core可以将它们分配给对象属性。调用SaveChanges后,所有Blog和Post对象都将具有正确的数据库生成的ID

相关问题