15

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

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

GiftCouponID 已在数据库中可用。当客户制作礼品券时,上面印有 GiftCouponID。操作员需要将此 CouponID 输入系统以进行付款。

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

  1. 礼券库
  2. 支付库

代码

//使用GiftCouponRepository获取对应的GiftCoupon对象。

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

参考:在 DDD 中,聚合应该代表事务边界。需要涉及多个聚合的交易通常表明模型应该被改进,或者交易需求应该被审查,或者两者兼而有之。CQRS 是否适合我的域?

在此处输入图像描述

C# 代码

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

4 回答 4

32

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

这是不可取的原因是因为并发性。除了保护其边界内的不变变量外,还应保护每个聚合免受并发事务的影响。例如,两个用户同时对聚合进行更改。

这种保护通常是通过在聚合的数据库表上设置版本/时间戳来实现的。保存聚合时,会比较正在保存的版本和当前存储在数据库中的版本(现在可能与事务开始时不同)。如果它们不匹配,则会引发异常。

基本上可以归结为:在一个协作系统中(很多用户进行很多事务),单个事务中修改的聚合越多,就会导致并发异常的增加。

如果您的聚合太大并提供许多状态更改方法,则完全相同;多个用户一次只能修改一个聚合。通过设计在事务中单独修改的小型聚合,可以减少并发冲突。

Vaughn Vernon 在他的 3 部分文章中出色地解释了这一点。

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

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

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

下面是我快速想出的一种可能的建模方式

  • 如果没有付款所属的订单汇总,我看不出付款的意义,所以我介绍了一个。
  • 订单由付款组成。可以使用礼券付款。您可以创建其他类型的付款,例如 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;
    }
}
于 2012-07-12T12:59:05.060 回答
2

在一个事务中使用两个存储库并没有错。正如 JB Nizet 所指出的,这就是服务层的用途。

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

1 EF/L2S DataContext本身就是一个 UoW 实现,但是对于此类情况,为服务层提供一个抽象的实现非常好。

于 2012-07-12T10:28:41.310 回答
0

我要提交的答案是“这取决于”(tm),因为它归结为“足够好”

问题空间和技术实现的背景都不是众所周知的,并且会影响任何可接受的解决方案。

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

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

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

注意:我没有定义聚合之间的交互是如何发生的,因为技术要求是未知的

  1. “创建”第一个聚合(我们称之为购买聚合),它将记录识别要使用的优惠券的预期付款。
  2. 尽可能晚地确认当前业务政策有效(每张优惠券当前有效)。如果没有,取消/停止业务交易。
  3. 将采购总量保持在“暂定”状态。
  4. 与每个优惠券集合交互以“调整限制”以进行试探性购买。回复成功/失败。
  5. “调整限制”将改变可用于其他潜在购买总量的可用金额
  6. 如果任何优惠券未能“调整限额”,则购买将“取消”,并且已批准的优惠券限额将重新调整回预购买请求金额(并且购买现在处于“取消”状态状态)
  7. 如果调整所有优惠券限额,则购买现在处于“完成”状态
  8. 在“完成”状态下,系统现在与每个优惠券聚合进行交互以“完成优惠券使用”,其中,购买的优惠券使用可能会记录在优惠券聚合上(取决于业务逻辑和需要)
  9. 一旦所有优惠券的使用都已完成,则购买总量设置为“已批准”状态,并且可以开始任何其他业务流程。

您的许多选择将取决于从业务和技术能力的角度来看什么是正确的。每个选择的利弊都会影响企业的成功,无论是现在还是未来。'这取决于'(tm)

于 2015-03-03T23:56:01.177 回答
0

2种方法:

  • 两个单独的交易。如果事务 2 失败,则事务 1 应该回滚。
  • 卡是一个帐户。针对该帐户记录交易。如果计算的余额(将所有交易相加)达到零(或更少,不应该发生),则卡已“使用”-但不要在数据库中记录“已使用”。只需从余额中得出。
于 2015-06-22T23:33:25.620 回答