一个事务中的多个聚合/存储库 [英] Multiple Aggregates / Repositories in one Transaction

查看:327
本文介绍了一个事务中的多个聚合/存储库的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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



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



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



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


  1. 礼品券库

  2. 付款存储库

代码


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


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


参考:在DDD中,Aggregate应该代表交易边界。需要多个聚合的参与的交易通常表明模型应该被改进,或者事务性要求应该被检查,或者两者都是。



然而,这只是一个指导原则,而且有不止一个聚合将需要修改。事实上,您正在考虑交易/使用案例是否可以重新构建为仅修改一个汇总是一件好事。



考虑到你的例子,我不能想到一种将其设计为满足交易/用例要求的单个聚合的方法。需要创建付款,需要更新优惠券以指示其不再有效。



但是,当真正分析这个事务的潜在并发问题时,我不认为礼品券实际上不会发生冲突骨料。他们只是创建(已发行)然后用于付款。之间没有其他状态变化的操作。因此,在这种情况下,我们不需要关心这一事实,我们正在修改付款/订单和礼品券聚合。



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




  • 如果没有订单合并付款,我看不出付款是否合理,所以我介绍了一个。

  • 订单由付款组成。可以使用礼品券付款。您可以创建其他类型的付款,例如CashPayment或CreditCardPayment。

  • 要作出礼券优惠券付款,息票汇总必须传递到订单汇总。

  • 在交易结束时,订单汇总将使用新的付款进行保存,并且还会保存所有使用的礼品券。 li>


代码:

  public class PaymentApplicationService 
{
public void PayForOrderWithGiftCoupons(PayForOrderWithGiftCouponsCommand命令)
{
using(IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
{
订单顺序= _orderRepository。 GetById(command.OrderId);

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

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

order.MakePaymentWithGiftCoupons(优惠券);

_orderRepository.Save(order);

foreach(优惠券中的GiftCoupon优惠券)
_giftCouponRepository.Save(coupon);
}
}
}

public class Order:IAggregateRoot
{
private readonly Guid _orderId;
私人只读列表<付款> _payments = new List< Payment>();

public Guid OrderId
{
get {return _orderId;}
}

public void MakePaymentWithGiftCoupons(List< GiftCoupon>优惠券)
{
foreach(优惠券中的GiftCoupon优惠券)
{
如果(!coupon.IsValid)
抛出新异常(优惠券不再有效);

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

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

public Guid PaymentId {get {return _paymentId; }}

public DateTime PaymentDate {get {return _paymentDate; }}

public abstract decimal数量{get;

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

public class GiftCouponPayment:payment
{
private readonly Guid _couponId;
私有readonly十进制_amount;

public override decimal金额
{
get {return _amount;


public GiftCouponPayment(Guid paymentId,DateTime paymentDate,GiftCoupon coupon)
:base(paymentId,paymentDate)
{
if(!coupon .IsValid)
抛出新异常(优惠券不再有效);

_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值
{
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;
}
}


I have a payment system as shown below. The payment can be made through multiple gift coupons. The gift coupons are issued along with a purchase. The customer can make use of this gift coupon for future purchase.

When a Payment is made through gift coupon, the UsedForPaymentID column in GiftCoupon table need to be updated with that PaymentID (for the giftcoupon ID).

The GiftCouponIDs are already available in the database. When a customer produces a gift coupon, it has GiftCouponID printed on it. The operator need to enter this CouponID to the system to make the Payment.

For the MakePayment() operation, it necessitates two repositories.

  1. Gift Coupon Repository
  2. Payment Repository

CODE

//Use GiftCouponRepository to retrieve the corresponding GiftCoupon object.

This involves use of two repositories for one transaction. Is it a good practice? If not, how can we change the design to overcome this?

Reference: In DDD the Aggregate should represent the transactional boundary. A transaction that requires the involvement of more than one aggregate is often a sign that either the model should be refined, or the transactional requirements should be reviewed, or both. Is CQRS correct for my domain?

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();
}

解决方案

I think what you really meant to ask was regarding 'Multiple Aggregates in one transaction'. I don't believe there is anything wrong with using multiple repositories to fetch data in a transaction. Often during a transaction an aggregate will need information from other aggregates in order to make a decision on whether to, or how to, change state. That's fine. It is, however, the modifying of state on multiple aggregates within one transaction that is deemed undesirable, and I think this what your referenced quote was trying to imply.

The reason this is undesirable is because of concurrency. As well as protecting the in-variants within it's boundary, each aggregate should be protected from concurrent transactions. e.g. two users making a change to an aggregate at the same time.

This protection is typically achieved by having a version/timestamp on the aggregates' DB table. When the aggregate is saved, a comparison is made of the version being saved and the version currently stored in the db (which may now be different from when the transaction started). If they don't match an exception is raised.

It basically boils down to this: In a collaborative system (many users making many transactions), the more aggregates that are modified in a single transaction will result in an increase of concurrency exceptions.

The exact same thing is true if your aggregate is too large & offers many state changing methods; multiple users can only modify the aggregate one at a time. By designing small aggregates that are modified in isolation in a transaction reduces concurrency collisions.

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

However, this is just a guiding principle and there will be exceptions where more than one aggregate will need to be modified. The fact that you are considering whether the transaction/use case could be re-factored to only modify one aggregate is a good thing.

Having thought about your example, I cannot think of a way of designing it to a single aggregate that fulfills the requirements of the transaction/use case. A payment needs to be created, and the coupon needs to be updated to indicate that it is no longer valid.

But when really analysing the potential concurrency issues with this transaction, I don't think there would ever actually be a collision on the gift coupon aggregate. They are only ever created (issued) then used for payment. There are no other state changing operations in between. Therefore in this instance we don't need to be concerned about that fact we are modifying both the payment/order & gift coupon aggregate.

Below is what I quickly came up with as a possible way of modelling it

  • I couldn't see how payments make sense without an order aggregate that the payment(s) belong to, so I introduced one.
  • Orders are made up of payments. A payment can be made with gift coupons. You could create other types of payments, such as CashPayment or CreditCardPayment for example.
  • To make a gift coupon payment, the coupon aggregates must be passed to the order aggregate. This then marks the coupon as used.
  • At the end of the transaction, the order aggregate is saved with its new payment(s), and any gift coupon used is also saved.

Code:

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;
    }
}

这篇关于一个事务中的多个聚合/存储库的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆