问题描述
我有一个付款系统,如下所示.可以通过多个礼品优惠券进行付款.礼品优惠券与购买一起发行.客户可以利用此礼品优惠券进行以后的购买.
当通过礼品优惠券进行付款时,需要使用paymentiD更新礼品电池表中的underforpaymentID列(对于礼品电池ID).
数据库中的礼品蛋白剂已经可用.当客户生产礼品优惠券时,它的礼品蛋白会印有.操作员需要将此优惠券输入系统以进行付款.
对于makepayment()操作,需要两个存储库.
- 礼品优惠券存储库
- 付款存储库
代码
//使用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
但是,这只是一个指导原则,需要修改一个以上的汇总.您正在考虑是否可以重新将交易/用例仅修改一个聚合是一件好事. 考虑了您的示例后,我无法想到将其设计到满足交易/用例要求的单个聚合的方法.需要创建付款,并且需要更新优惠券,以表明它不再有效. 但是,当真正分析此交易的潜在并发问题时,我认为实际上不会在礼品优惠券总体上发生碰撞.它们仅是创建(已发行)然后用于付款的.两者之间没有其他国家改变运营.因此,在这种情况下,我们不需要担心这个事实,我们正在修改付款/订单和礼品优惠券汇总. 以下是我迅速提出的作为建模的可能方法 代码: 在一次交易中使用两个存储库没有错.正如JB Nizet指出的那样,这就是服务层的目的. 如果您有问题保持连接共享,则可以使用工作单位 1 模式以控制服务层的连接,并拥有向您的存储库提供数据上下文的工厂. 1 ef/l2s datacontext是本身 uow实现,但是对于这样的情况,对于这样的情况,有一个抽象的服务层. 我要提交的答案将是"取决于"(TM),因为它归结为"足够好" 问题空间和技术实施的上下文尚不清楚,并且会影响任何可接受的解决方案. 如果这些技术允许(例如在酸数据存储中),那么从业务角度来看,使用交易可能是有意义的. 如果技术不提供这些功能,那么"锁定"所有优惠券和付款记录可能是有意义的,以使更新保持一致.需要调查锁定时间和可能发生什么争议. 第三,它可以用以下粗糙业务流程策略作为多个交易/汇总实施. 注意:我没有定义汇总之间的相互作用是如何发生的,因为技术要求尚不清楚 您的许多选择将取决于从业务和技术能力的角度来看的正确选择.现在或将来,每个选择的专业人士和骗局都会影响业务的成功. '这取决于'(tm)
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;
}
}
其他推荐答案
其他推荐答案
问题描述
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.
- Gift Coupon Repository
- 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; } }
其他推荐答案
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
- '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.
- As late as possible, confirm that the current business policies are valid (each coupon is currently valid). If not, cancel/stop the business transaction.
- Persist the purchase aggregate in a 'tentative' state.
- interact with each coupon aggregate to 'adjust limit' for the tentative purchase. Reply back with success/failure.
- the 'adjust limit' will change the available amount of money that is available for other potential purchase aggregates
- 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)
- If all coupons limit are adjusted, then the purchase is now in an 'finalizing' state
- 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)
- 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)