一个事务中的多个聚合/存储库

时间:2012-07-12 05:46:47

标签: c# oop design-patterns domain-driven-design cqrs

我有一个支付系统,如下所示。付款可以通过多个礼券进行。礼品券与购买一起发行。客户可以使用此礼券以备将来购买。

当通过礼品券付款时,GiftCoupon表中的UsedForPaymentID列需要使用该PaymentID更新(对于礼品券ID)。

GiftCouponID已在数据库中提供。当客户生产礼品券时,它上面印有GiftCouponID。运营商需要将此CouponID输入系统以进行付款。

对于MakePayment()操作,它需要两个存储库。

  1. 礼品券库
  2. 付款存储库
  3. CODE

      

    //使用GiftCouponRepository检索相应的GiftCoupon对象。

    这涉及在一个事务中使用两个存储库。这是一个好习惯吗?如果没有,我们如何改变设计来克服这个问题?

      

    参考:在DDD中,Aggregate应代表事务边界。需要涉及多个聚合的交易通常表明应该改进模型,或者应该审查交易要求,或者两者兼而有之。 Is CQRS correct for my domain?

    enter image description here

    C#CODE

    public RepositoryLayer.ILijosPaymentRepository repository { get; set; }
    
    public void MakePayment(int giftCouponID)
    {
        DBML_Project.Payment paymentEntity = new DBML_Project.Payment();
        paymentEntity.PaymentID = 1;
    
        DBML_Project.GiftCoupon giftCouponObj;
    
        //Use GiftCouponRepository to retrieve the corresponding GiftCoupon object.     
    
        paymentEntity.GiftCouponPayments = new System.Data.Linq.EntitySet<DBML_Project.GiftCoupon>();
        paymentEntity.GiftCouponPayments.Add(giftCouponObj);
    
        repository.InsertEntity(paymentEntity);
        repository.SubmitChanges();
    }
    

4 个答案:

答案 0 :(得分:29)

我认为您真正要问的是“一次交易中的多个聚合”。我不认为使用多个存储库来获取事务中的数据有什么问题。通常在交易期间,聚合将需要来自其他聚合的信息,以便决定是否或如何改变状态。没关系。但是,在一个事务中,多个聚合上的状态修改被认为是不合需要的,我认为这是您引用的引用所暗示的内容。

这是不受欢迎的原因是因为并发。除了保护其边界内的in-variants外,还应保护每个聚合不受并发事务的影响。例如两个用户同时对聚合进行更改。

通常通过在聚合的DB表上设置版本/时间戳来实现此保护。保存聚合时,将比较正在保存的版本和当前存储在db中的版本(现在可能与事务启动时不同)。如果它们不匹配则引发异常。

它基本归结为:在协作系统(许多用户进行多次交易)中,在单个事务中修改的聚合越多,将导致并发异常的增加。

如果你的总量太大而且完全相同的情况也是如此。提供许多状态改变方法;多个用户一次只能修改一个聚合。通过设计在事务中单独修改的小聚合可以减少并发冲突。

Vaughn Vernon has done an excellent job explaining this in his 3 part article.

但是,这只是一个指导原则,并且会有例外情况需要修改多个聚合。您正在考虑是否可以重新考虑事务/用例以仅修改一个聚合这一事实是一件好事。

考虑过您的示例后,我无法想到将其设计为满足事务/用例要求的单个聚合的方法。需要创建付款,并且需要更新优惠券以表明它不再有效。

但在真正分析交易的潜在并发性问题时,我认为礼券优惠券实际上不会发生冲突。它们只是创建(发行)然后用于付款。两者之间没有其他状态改变操作。因此,在这种情况下,我们不需要担心我们正在修改付款/订单和礼品券。

以下是我很快想出的一种可能的建模方法

  • 如果没有付款所属的订单汇总,我看不出付款是否合理,所以我介绍了一个。
  • 订单由付款组成。可以使用礼券进行付款。您可以创建其他类型的付款,例如CashPayment或CreditCardPayment。
  • 要进行礼品券付款,优惠券总额必须传递到订单汇总。然后,这标记了使用的优惠券。
  • 在交易结束时,订单汇总将与其新付款一起保存,并且还会保存使用的任何礼品券。

代码:

public class PaymentApplicationService
{
    public void PayForOrderWithGiftCoupons(PayForOrderWithGiftCouponsCommand command)
    {
        using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
        {
            Order order = _orderRepository.GetById(command.OrderId);

            List<GiftCoupon> coupons = new List<GiftCoupon>();

            foreach(Guid couponId in command.CouponIds)
                coupons.Add(_giftCouponRepository.GetById(couponId));

            order.MakePaymentWithGiftCoupons(coupons);

            _orderRepository.Save(order);

            foreach(GiftCoupon coupon in coupons)
                _giftCouponRepository.Save(coupon);
        }
    }
}

public class Order : IAggregateRoot
{
    private readonly Guid _orderId;
    private readonly List<Payment> _payments = new List<Payment>();

    public Guid OrderId 
    {
        get { return _orderId;}
    }

    public void MakePaymentWithGiftCoupons(List<GiftCoupon> coupons)
    {
        foreach(GiftCoupon coupon in coupons)
        {
            if (!coupon.IsValid)
                throw new Exception("Coupon is no longer valid");

            coupon.UseForPaymentOnOrder(this);
            _payments.Add(new GiftCouponPayment(Guid.NewGuid(), DateTime.Now, coupon));
        }
    }
}

public abstract class Payment : IEntity
{
    private readonly Guid _paymentId;
    private readonly DateTime _paymentDate;

    public Guid PaymentId { get { return _paymentId; } }

    public DateTime PaymentDate { get { return _paymentDate; } }

    public abstract decimal Amount { get; }

    public Payment(Guid paymentId, DateTime paymentDate)
    {
        _paymentId = paymentId;
        _paymentDate = paymentDate;
    }
}

public class GiftCouponPayment : Payment
{
    private readonly Guid _couponId;
    private readonly decimal _amount;

    public override decimal  Amount
    {
        get { return _amount; }
    }

    public GiftCouponPayment(Guid paymentId, DateTime paymentDate, GiftCoupon coupon)
        : base(paymentId, paymentDate)
    {
        if (!coupon.IsValid)
            throw new Exception("Coupon is no longer valid");

        _couponId = coupon.GiftCouponId;
        _amount = coupon.Value;
    }
}

public class GiftCoupon : IAggregateRoot
{
    private Guid _giftCouponId;
    private decimal _value;
    private DateTime _issuedDate;
    private Guid _orderIdUsedFor;
    private DateTime _usedDate;

    public Guid GiftCouponId
    {
        get { return _giftCouponId; }
    }

    public decimal Value
    {
        get { return _value; }
    }

    public DateTime IssuedDate
    {
        get { return _issuedDate; }
    }

    public bool IsValid
    {
        get { return (_usedDate == default(DateTime)); }
    }

    public void UseForPaymentOnOrder(Order order)
    {
        _usedDate = DateTime.Now;
        _orderIdUsedFor = order.OrderId;
    }
}

答案 1 :(得分:2)

在一个事务中使用两个存储库没有任何问题。正如JB Nizet指出的那样,这就是服务层的用途。

如果您在保持连接共享时遇到问题,可以使用Unit of Work 1 模式来控制来自服务层的连接,并让工厂提供数据上下文您的存储库提供OoW实例。

1 EF / L2S DataContext 本身是一个UoW实现,但对于这些情况,服务层有一个抽象的很好。

答案 2 :(得分:0)

我提交的答案将取决于&#39;(tm),因为它归结为足够好&#39;

问题空间和技术实施的背景并不为人所知,并会影响任何可接受的解决方案。

如果技术允许它(比如在ACID数据存储中),那么从业务角度来看,使用事务可能是有意义的。

如果技术不提供这些功能,那么锁定&#39;所有优惠券和付款记录,以使更新保持一致。需要调查多长时间的锁定以及可能发生的争用。

第三,它可以实现为具有以下粗略业务流程策略的多个事务/聚合。

注意:由于技术要求未知,我没有定义聚合之间的交互方式

  1. &#39;创建&#39;第一个汇总(让我们称之为购买汇总),它将记录识别要使用的优惠券的预期付款。
  2. 尽可能晚,确认当前的商业政策有效(每张优惠券目前有效)。如果没有,请取消/停止业务交易。
  3. 将购买总量保留在“试用期”中。状态。
  4. 与每个优惠券汇总互动,以调整限额&#39;暂时购买。回复成功/失败。
  5. 调整限制&#39;将更改可用于其他潜在购买总量的可用金额
  6. 如果任何优惠券未能调整限额,则购买将被取消&#39;并且已批准的优惠券限额会重新调整回购买前请求金额(并且购买现已处于“取消状态”)
  7. 如果调整了所有优惠券限额,那么购买现在正在最终确定&#39;状态
  8. 在最终确定&#39;状态,系统现在与每个优惠券聚合进行交互,以最终确定优惠券使用情况&#39;在可能的情况下,购买的优惠券使用情况记录在优惠券总量上(取决于业务逻辑和需求)
  9. 一旦完成所有优惠券使用,则将购买总量设置为“已批准”的状态。并且可以开始任何其他业务流程。
  10. 您的许多选择将取决于业务和技术能力方面的正确性。无论是现在还是将来,每个选择的专业人士和成员都会影响业务的成功。 &#39;这取决于&#39;(tm)

答案 3 :(得分:0)

2种方法:

  • 两个单独的交易。如果事务2失败,则应回滚事务1。
  • 卡是一个帐户。记录该帐户的交易。如果计算余额(累计所有交易)达到零(或更少,不应该发生),则卡被“使用” - 不要在数据库中记录“已使用”。只是从平衡中得出它。