如何在Clean Architecture中使用事务?

时间:2018-06-15 07:44:05

标签: architecture clean-architecture

我在网上找不到它的实现实际上给你一个框架无关和实用的实现方式。

我已经看到了几个解决问题的低级建议:

  1. 使存储库方法成为原子

  2. 使用例子原子

  3. 它们都不是理想的。

    案例#1 :大多数用例依赖于多个Repository方法来完成工作。当您“下订单”时,您可能必须调用“订单存储库”的“插入”方法和“用户存储库”的“更新”方法(例如:扣除商店信用)。如果“插入”和“更新”是原子的,这将是灾难性的 - 您可以下订单,但实际上无法让用户付费。或者让用户付费,但订单失败。两者都不理想。

    案例#2 :不是更好。如果每个用例都存在于一个孤岛中,它会起作用,但除非您想要复制代码,否则您经常会发现自己拥有依赖于其他用例操作的用例。

    想象一下,您有一个“下订单”用例和一个“给予奖励积分”用例。两个用例都可以单独使用。例如,当老板在系统发布日的周年纪念日登录时,老板可能希望为系统中的每个用户“提供奖励积分”。当然,只要用户进行购买,您就会使用“下订单”用例。

    现在你的系统发布10周年了。你的老板决定 - “好吧Jimbo - 对于2018年7月,每当有人下订单时,我都想给予奖励积分”。

    为了避免直接改变“下订单”用例,这个一次性的想法可能会在明年被放弃,你决定要创建另一个用例(“促销期间的下订单”)只需拨打“下订单”和“给予奖励积分”。精彩。

    只有......你做不到。我的意思是,你可以。但你又回到原点了。您可以保证“下订单”是否成功,因为它是原子的。并且您可以保证“奖励积分”是否因同样的原因而成功。但如果其中任何一个失败,你就无法回击另一个。它们不共享相同的事务上下文(因为它们在内部“开始”和“提交”/“回滚”事务)。

    上面的场景有一些可能的解决方案,但它们都不是非常“干净”(工作单元浮现在脑海中 - 在用例之间共享一个工作单元可以解决这个问题,但UoW是一个丑陋的模式,并且仍然存在知道哪个用例负责打开/提交/回滚事务的问题。

4 个答案:

答案 0 :(得分:2)

我把交易放在控制器上。控制器知道更大的框架,因为它可能至少具有类似框架注释的元数据。

关于工作单元,这是个好主意。您可以让每个用例启动一个事务。在内部,工作单元要么启动实际事务,要么增加调用启动的计数器。然后每个用例都会调用commit或reject。当提交计数等于0时,调用实际提交。拒绝跳过所有这些,回滚,然后错误输出(异常或返回代码)。

在您的示例中,包装用例调用start(c = 1),地点顺序调用start(c = 2),下订单提交(c = 1),奖励调用start(c = 2),奖励调用commit (c = 1),包装提交(c = 0)所以实际提交。

我将子交易留给你。

答案 1 :(得分:0)

案例#1

  

当您下订单时,您可能需要拨打"插入"   " Order Repository"的方法并且"更新" "用户的方法   储存库" (例如:扣除商店信用)。

这两种方法都应该是原子的,整个用例是一个工作单元。工作单元的工作是确保两种方法成功或失败。

模式

  

"工作单元会跟踪您在业务中所做的一切   可能影响数据库的事务。当你完成时,它会有所体现   结果需要做的一切都是为了改变数据库   你的工作。" - Martin Fowler

如果您正在寻找一个捕获所有工作单元/存储库模式,您将无法找到一个,如果您正在寻找与框架无关的实现,则更不可能。

这是关于互联网的最诚实的实体框架存储库和UoW教程 (摘自:https://docs.microsoft.com/en-us/aspnet/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application

  

有许多方法可以实现存储库和工作单元   图案。您可以使用包含或不包含单位的存储库类   工人阶级。您可以为所有实体实现单个存储库   类型,或每种类型一个。如果你为每种类型实现一个,你   可以使用单独的类,通用基类和派生类,或   抽象基类和派生类。你可以包括业务   存储库中的逻辑或将其限制为数据访问逻辑。您可以   还可以通过构建数据库上下文类的抽象层   使用IDbSet接口而不是实体的DbSet类型   集。实现抽象层的方法如下所示   教程是您可以考虑的一个选项,而不是建议   所有场景和环境。

开箱即用,OR / M通常提供某种形式的存储库和工作单元格式,这些模式已经为您抽象出来,并且实现对消费者(您)是隐藏的。

如果您需要在XML中保存数据,那么您的存储库可能看起来非常相似,但您的UoW看起来会有很大不同,并且您不会有任何事务/回滚支持。< / p>

&#34;清洁代码&#34;方法是为您提供满足SOLID原则所需的多个抽象级别。

有一个非常好的视频,介绍如何在不实际依赖任何持久性框架的情况下构建您的存储库和UoW,但我不会将其称为完全框架不可知,因为它闻起来像实体框架。

Mosh Hamedani 使用C#和实体框架的存储库模式,完成正确 https://www.youtube.com/watch?v=rtXpYpZdOzM

案例#2 在我看来存在潜在的架构问题。

LoginController需要&#34;登录用户&#34;和&#34;给予奖励积分&#34; - 做两件事。

PurchaseController需要&#34;完成购买&#34;和&#34;给予奖励积分&#34; - 做两件事

解决方案是创建一个RewardsPointsController,并让LoginController和PurchaseController调用RewardsPointsController来决定用户是否根据事件类型或日期获得奖励积分。

如果你这样想的话就会变得更清楚了。

  

如果用户在促销期间登录,并且RewardsPointsController无法添加奖励积分,那么整个交易是否会失败?然后用户将无法登录 - 以后可以通过客户服务电话修复奖励积分,但如果用户无法登录或完成购买,则您已经失去了业务。< / p>

答案 2 :(得分:0)

通常,建议将事务定义放在UseCase层中,因为它具有适当的抽象级别并且具有并发要求。 我认为最好的解决方案是您在案例2中公开的解决方案。为了解决重用不同UseCases的问题,某些框架使用了Transaction中的概念传播。例如,在Spring中,您可以将交易定义为REQUIRED或REQUIRES_NEW。

https://www.byteslounge.com/tutorials/spring-transaction-propagation-tutorial

如果您在UseCase PlaceOrderAndGiveAwards中定义了Transactional REQUIRED,则该事务将在“基本”用例中重用,并且内部方法中的回滚将使空洞事务回滚

答案 3 :(得分:0)

我知道这是个老问题, 但希望此答案将对寻求示例实施的人有所帮助 由于清理拱门,所有指向向内而不是向外

的图层

All Layers pointing inward

因此,由于交易是应用程序层的关注点,因此我将其保留在应用程序层中。

在快速示例中,我使应用程序层具有接口IDBContext 有我将使用的所有Dbset 如下

public interface IDBContext
{
    DbSet<Blog> Blogs { get; set; }
    DbSet<Post> Posts{ get; set; }
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
    DatabaseFacade datbase { get; }
}

在持久层中,我已经实现了该接口

public class ApplicationDbContext : DbContext, IDBContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) :base(options)
    {

    }
    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }
    public DatabaseFacade datbase => Database;


    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken())
    {
        
        var result = await base.SaveChangesAsync(cancellationToken);
        return result;
    }
}

返回我通常使用IMediator并遵循CQRM的应用程序层 所以我做了这个例子,希望对你有帮助 这是我开始交易的那一行

await using var transaction = await context.datbase.BeginTransactionAsync();

这是我正在使用事务的命令处理程序

public async Task<int> Handle(TransactionCommand request, CancellationToken cancellationToken)
    {
        int updated = 0;
        await using var transaction = await context.datbase.BeginTransactionAsync();
        try
        {
            var blog = new Core.Entities.Blog { Url = $"Just test the number sent = {request.number}" };
            await context.Blogs.AddAsync(blog);
            await context.SaveChangesAsync(cancellationToken);

            for (int i = 0; i < 10; i++)
            {
                var post = new Core.Entities.Post
                {
                    BlogId = blog.BlogId,
                    Title = $" Title {i} for {blog.Url}"
                };
                await context.Posts.AddAsync(post); 
                await context.SaveChangesAsync(cancellationToken);
                updated++;
            }

            var divresult = 5 / request.number;
            await transaction.CommitAsync();
            
        }
        catch (Exception ex)
        {
            var msg = ex.Message;
            return 0;
            
        }
        return updated;


    }

这是我刚创建的用于详细说明答案的示例的Link

请记住,我在大约15分钟内完成了此示例,以防万一有些命名错误时要参考:)

此致