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

本文是小编为大家收集整理的关于一个交易中的多个聚合体/存储库的处理/解决方法,可以参考本文帮助大家快速定位并解决问题,中文翻译不准确的可切换到English标签页查看源文。

问题描述

我有一个付款系统,如下所示.可以通过多个礼品优惠券进行付款.礼品优惠券与购买一起发行.客户可以利用此礼品优惠券进行以后的购买.

当通过礼品优惠券进行付款时,需要使用paymentiD更新礼品电池表中的underforpaymentID列(对于礼品电池ID).

数据库中的礼品蛋白剂已经可用.当客户生产礼品优惠券时,它的礼品蛋白会印有.操作员需要将此优惠券输入系统以进行付款.

对于makepayment()操作,需要两个存储库.

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

代码

//使用GiftCouponRepository检索相应的礼品电池对象.

这涉及将两个存储库用于一次交易.这是一个好习惯吗?如果没有,我们如何更改设计以克服这一点?

参考:在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();
}

推荐答案

我认为您真正要问的是关于'一项交易中的多个聚合'.我不认为使用多个存储库在交易中获取数据有什么问题.通常,在交易中,汇总会需要其他骨料的信息,以便决定是否或如何改变状态.没关系.但是,这是对一项交易中多个汇总的状态修改,认为这是不受欢迎的,我认为这是您所引用的报价所暗示的.

这是不可取的原因是由于并发.除了保护其边界内的内置体外,还应保护每个骨料免受并发交易的保护.例如两个用户同时更改聚合.

通常,通过在聚合的DB表上拥有版本/时间戳来实现此保护.保存聚合物后,将进行比较与要保存的版本以及当前存储在DB中的版本(现在可能与交易开始时有所不同).如果他们不匹配例外.

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

如果您的汇总太大并提供许多状态变化的方法,则完全相同的事情是正确的.多个用户只能一次修改一个用户.通过设计在交易中孤立修改的小骨料来减少并发碰撞.

沃恩·弗农(Vaughn Vernon

但是,这只是一个指导原则,需要修改一个以上的汇总.您正在考虑是否可以重新将交易/用例仅修改一个聚合是一件好事.

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

但是,当真正分析此交易的潜在并发问题时,我认为实际上不会在礼品优惠券总体上发生碰撞.它们仅是创建(已发行)然后用于付款的.两者之间没有其他国家改变运营.因此,在这种情况下,我们不需要担心这个事实,我们正在修改付款/订单和礼品优惠券汇总.

以下是我迅速提出的作为建模的可能方法

  • 我看不到没有付款属于付款的订单,我介绍了一项.
  • 订单由付款组成.可以用礼品优惠券付款.您可以创建其他类型的付款,例如现金付款或信用卡款.
  • 要支付礼品优惠券,优惠券汇总必须将其传递给订单汇总.然后,这标记了使用的优惠券.
  • 在交易结束时,订单总额通过其新付款保存,并且还保存了任何礼品优惠券.

代码:

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

其他推荐答案

在一次交易中使用两个存储库没有错.正如JB Nizet指出的那样,这就是服务层的目的.

如果您有问题保持连接共享,则可以使用工作单位 1 模式以控制服务层的连接,并拥有向您的存储库提供数据上下文的工厂.

1 ef/l2s datacontext是本身 uow实现,但是对于这样的情况,对于这样的情况,有一个抽象的服务层.

其他推荐答案

我要提交的答案将是"取决于"(TM),因为它归结为"足够好"

问题空间和技术实施的上下文尚不清楚,并且会影响任何可接受的解决方案.

如果这些技术允许(例如在酸数据存储中),那么从业务角度来看,使用交易可能是有意义的.

如果技术不提供这些功能,那么"锁定"所有优惠券和付款记录可能是有意义的,以使更新保持一致.需要调查锁定时间和可能发生什么争议.

第三,它可以用以下粗糙业务流程策略作为多个交易/汇总实施.

注意:我没有定义汇总之间的相互作用是如何发生的,因为技术要求尚不清楚

  1. '创建'第一个汇总(让我们称其为"购买汇总"),它将记录确定要使用的优惠券的预期付款.
  2. 尽可能晚,确认当前的业务政策是有效的(每个优惠券当前有效).如果没有,请取消/停止业务交易.
  3. 将购买总额持续到"暂定"状态.
  4. 与每个优惠券的互动以进行暂定购买的"调整限制".成功/失败回复.
  5. "调整限制"将更改其他潜在购买汇总的可用资金
  6. 如果任何优惠券无法"调整限制",则购买的购买是"被取消",并且已批准的优惠券限制将重新调整回购买前请求金额(现在购买已在"取消"状态)
  7. 如果调整了所有优惠券限制,则购买现在为"最终化"状态
  8. 在"最终确定"状态下,该系统现在与每个优惠券总体进行交互,以"最终确定优惠券使用",在此可能,购买的优惠券使用情况在优惠券汇总(取决于业务逻辑和需求)
  9. 一旦所有优惠券的使用量都完成了,则将购买汇总设置为"批准"的状态,任何其他业务流程都可以开始.

您的许多选择将取决于从业务和技术能力的角度来看的正确选择.现在或将来,每个选择的专业人士和骗局都会影响业务的成功. '这取决于'(tm)

本文地址:https://www.itbaoku.cn/post/627538.html

问题描述

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?

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

推荐答案

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

其他推荐答案

There's nothing wrong with using two repositories in one transaction. As JB Nizet points out, that's what a service layer is for.

If you're having issue keeping the connection shared, you can use the Unit of Work1 pattern to control the connection from the service layer and have the factory that provides the data context to your repositories supply the OoW instance.

1 The EF/L2S DataContext is itself a UoW implementation, but it's nice to have an abstract one for the service layer for situations such as these.

其他推荐答案

The answer I would submit would be 'it depends'(tm) as it comes down to what is 'good enough'

The context of both the problem space and the technical implementation are not well known and will affect any acceptable solution.

If the technologies allow it (say in a ACID data store), then it might make sense from a business perspective to use a transaction.

If the technologies do not provide these capabilities, then it might make sense to 'lock' all the coupons and the payments records in order for the updates to be consistent. How long of a lock and what contention might be occurring would need to be investigated.

Thirdly, it could be implemented as multiple transactions/aggregates with the following rough business process strategy.

Note: I'm not defining how the interaction is happening between the aggregates as the technical requirements are not known

  1. 'Create' the first aggregate (let's call it the purchase aggregate), which will record the expected payments which identify the coupon(s) to be used.
  2. As late as possible, confirm that the current business policies are valid (each coupon is currently valid). If not, cancel/stop the business transaction.
  3. Persist the purchase aggregate in a 'tentative' state.
  4. interact with each coupon aggregate to 'adjust limit' for the tentative purchase. Reply back with success/failure.
  5. the 'adjust limit' will change the available amount of money that is available for other potential purchase aggregates
  6. If any of the coupons fail to 'adjust limit', then the purchase is 'being cancelled' and the coupon limits that were approved are re-adjusted back to the pre-purchase request amounts (and the purchase is now in a 'cancelled' state)
  7. If all coupons limit are adjusted, then the purchase is now in an 'finalizing' state
  8. in the 'finalizing' state, the system now interacts with each coupon aggregate to 'finalize coupon usage' where, possibly, the coupon usage for the purchase is journaled on the coupon aggregate (depends on business logic and need)
  9. once all the coupon usages have been finalized, then the purchase aggregate is set to the state of 'approved' and any additional business processes can commence.

A lot of your choices will depend on what is correct from a business and technical capabilities perspective. The pro's and con's of each choice affect the business's success, either now or in the future. 'It depends'(tm)