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

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

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

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

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

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

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

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

参考 :在DDD中,Aggregate应代表事务边界。 需要涉及多个聚合的交易通常表明应该改进模型,或者应该审查交易要求,或者两者兼而有之。 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(); paymentEntity.GiftCouponPayments.Add(giftCouponObj); repository.InsertEntity(paymentEntity); repository.SubmitChanges(); } 

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

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

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

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

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

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 coupons = new List(); 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 _payments = new List(); public Guid OrderId { get { return _orderId;} } public void MakePaymentWithGiftCoupons(List 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; } } 

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

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

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

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

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

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

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

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

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

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

您的许多选择将取决于业务和技术能力方面的正确性。 无论是现在还是将来,每种选择的专业人士和企业都会影响企业的成功。 ‘这取决于’(tm)

2种方法:

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