Wiele agregatów / repozytoriów w jednej transakcji

Mam system płatności, jak pokazano poniżej. Płatność może być dokonana za pomocą wielu kuponów upominkowych. Kupony upominkowe są wydawane wraz z zakupem. Klient może skorzystać z tego kuponu upominkowego do przyszłego zakupu.

Gdy płatność jest dokonywana za pomocą kuponu upominkowego, Kolumna UsedForPaymentID w tabeli GiftCoupon musi zostać zaktualizowana o ten identyfikator PaymentID(dla identyfikatora giftcoupon).

Giftcouponidy są już dostępne w bazie danych. Kiedy klient produkuje kupon upominkowy, ma nadrukowany GiftCouponID. Operator musi wprowadzić ten identyfikator kuponu do systemu, aby dokonać płatności.

Operacja MakePayment() wymaga dwóch repozytoriów.

  1. Repozytorium Kuponów Upominkowych
  2. Repozytorium Płatności

Kod

//Użyj GiftCouponRepository, aby pobrać odpowiedni obiekt GiftCoupon.

Polega to na wykorzystaniu dwóch repozytoriów dla jednej transakcji. Czy to dobra praktyka? Jeśli nie, jak możemy zmienić projekt, aby to przezwyciężyć?

Odniesienie: W DDD Agregat powinien reprezentować granicę transakcji. Transakcja, która wymaga zaangażowania więcej niż jednego agregatu, jest często znakiem, że model powinien zostać udoskonalony, albo wymagania transakcyjne powinny zostać poddane przeglądowi, albo oba. czy CQRS jest poprawny dla mojej domeny?

Tutaj wpisz opis obrazka

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();
}
Author: Community, 2012-07-12

4 answers

Myślę, że naprawdę chciałeś zapytać o " Wiele agregatów w jednej transakcji ". Nie wierzę, że jest coś złego w używaniu wielu repozytoriów do pobierania danych w transakcji . Często podczas transakcji agregat będzie potrzebował informacji z innych agregatów, aby podjąć decyzję o tym, czy zmienić stan lub jak go zmienić. W porządku. Jest to jednak modyfikacja stanu na wielu agregatach w ramach jednej transakcji, która jest uważana niepożądane, i myślę, że to, co cytat próbował sugerować.

Jest to niepożądane ze względu na współbieżność. Oprócz ochrony wariantów in w jego granicach, każdy agregat powinien być chroniony przed jednoczesnymi transakcjami. np. dwóch użytkowników dokonujących zmiany w agregacie w tym samym czasie.

Ochrona ta jest zazwyczaj uzyskiwana poprzez umieszczenie znacznika wersji/czasu w tabeli dB agregatów. Po zapisaniu agregatu dokonuje się porównania wersji zapisywanej i aktualnie przechowywanej w bazie danych (która może być inna niż w momencie rozpoczęcia transakcji). Jeśli nie pasują one wyjątek jest podniesiony.

Sprowadza się to zasadniczo do tego: w systemie współpracy (wielu użytkowników dokonujących wielu transakcji), im więcej agregatów, które są modyfikowane w jednej transakcji, spowoduje wzrost WYJĄTKÓW w współbieżności.

Dokładnie to samo jest prawdą, jeśli Agregat jest zbyt duży i oferuje wiele metody zmiany stanu; wielu użytkowników może modyfikować agregat tylko jeden na raz. Projektując małe agregaty, które są modyfikowane osobno w transakcji, zmniejsza kolizje współbieżności.

Vaughn Vernon wykonał świetną robotę wyjaśniając to w swoim 3-częściowym artykule.

Jest to jednak tylko zasada przewodnia i będą wyjątki, w których więcej niż jeden agregat będzie musiał zostać zmodyfikowany. Fakt, że rozważasz, czy transakcja / korzystanie przypadek może być ponownie uwzględniony, aby zmodyfikować tylko jeden agregat to dobra rzecz.

Myśląc o twoim przykładzie, nie mogę wymyślić sposobu zaprojektowania go do jednego agregatu, który spełnia wymagania przypadku transakcji / użycia. Należy utworzyć płatność, a kupon należy zaktualizować, aby zaznaczyć, że nie jest już ważny.

Ale naprawdę analizując potencjalne problemy z współbieżnością z tą transakcją , nie sądzę, aby kiedykolwiek doszło do kolizja na kupon upominkowy. Są one tylko tworzone (wydawane) , a następnie wykorzystywane do płatności. Nie ma innych operacji zmieniających stan pomiędzy. Dlatego w tym przypadku nie musimy martwić się o to, że modyfikujemy zarówno płatność / zamówienie, jak i kupon upominkowy.

Poniżej jest to, co szybko wymyśliłem jako możliwy sposób modelowania tego

  • nie mogłem zrozumieć, w jaki sposób płatności mają sens bez agregatu zamówienia, do którego należą płatności, więc przedstawiłem jednego.
  • zamówienia składają się z płatności. Płatność może być dokonana za pomocą kuponów upominkowych. Możesz tworzyć inne rodzaje płatności, takie jak płatność gotówkowa lub Płatność kartą kredytową.
  • aby dokonać płatności kuponem upominkowym, Agregaty kuponów muszą zostać przekazane do agregatu zamówienia. Oznacza to, że kupon jest używany.
  • Po zakończeniu transakcji agregat zamówienia jest zapisywany wraz z nową płatnością(nowymi płatnościami), a każdy wykorzystany kupon upominkowy jest również uratowany.

Kod:

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;
    }
}
 28
Author: David Masters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-12 15:46:02

Nie ma nic złego w używaniu dwóch repozytoriów w jednej transakcji. Jak podkreśla JB Nizet, po to jest warstwa usług.

Jeśli masz problem z utrzymaniem współdzielonego połączenia, możesz użyć jednostki pracy1 wzorzec do kontrolowania połączenia z warstwy usług i fabrycznie dostarczający kontekst danych do repozytoriów dostarcza wystąpienie OoW.

1 EF / L2S DataContext jest Sam implementacją UoW, ale miło jest mieć abstrakcyjny dla warstwy usług w takich sytuacjach.

 2
Author: Richard Szalay,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-07-12 10:34:13

Odpowiedź, którą chciałbym złożyć, brzmiałaby "to zależy"(tm), ponieważ sprowadza się do tego, co jest "wystarczająco dobre"

Kontekst zarówno przestrzeni problemowej, jak i technicznej implementacji nie są dobrze znane i będą miały wpływ na każde akceptowalne rozwiązanie.

Jeśli technologie na to pozwalają (np. w magazynie danych ACID), to z biznesowego punktu widzenia korzystanie z transakcji może mieć sens.

Jeśli technologie nie zapewniają tych możliwości, to może mieć sens "zablokowanie" wszystkich kupony i zapisy płatności, aby aktualizacje były spójne. Jak długo trwa blokada i jakie mogą wystąpić spory należy zbadać.

Po trzecie, może być wdrożony jako wiele transakcji / agregatów z następującą szorstką strategią procesu biznesowego.

uwaga: nie definiuję interakcji między agregatami, ponieważ wymagania techniczne nie są znane

  1. 'Create' the first aggregate (let ' s call it the purchase aggregate), która będzie rejestrować oczekiwane płatności, które identyfikują kupon(y) do wykorzystania.
  2. tak późno, jak to możliwe, potwierdzić, że aktualne zasady biznesowe są ważne (każdy kupon jest obecnie ważny). Jeśli nie, anuluj / zatrzymaj transakcję biznesową.
  3. interakcja z każdym zbiorczym kuponem, aby "dostosować limit" dla wstępnego zakupu. Odpowiedz z sukcesem / porażką.
  4. the "dostosuj limit" zmieni dostępną kwotę pieniędzy, która jest dostępna dla innych potencjalnych agregatów zakupowych
  5. Jeśli którykolwiek z kuponów nie "dostosuje limitu", zakup jest "anulowany", a zatwierdzone limity kuponów są ponownie dostosowywane do kwot żądania przed zakupem (a zakup jest teraz w stanie "anulowanym")
  6. jeśli limit wszystkich kuponów zostanie skorygowany, zakup jest teraz w stanie "finalizacji"
  7. w stanie "finalizacji", system współdziała teraz z każdym zbiorczym kuponem, aby "sfinalizować użycie kuponu", gdzie ewentualnie użycie kuponu do zakupu jest rejestrowane na zbiorczym kuponie (zależy od logiki biznesowej i potrzeby) {]}
  8. po sfinalizowaniu wszystkich kuponów, agregat zakupowy jest ustawiony na stan "zatwierdzony" i można rozpocząć wszelkie dodatkowe procesy biznesowe.

Wiele twoich wyborów będzie zależeć od tego, co jest poprawne z biznesu i możliwości technicznych perspektywa. Profesjonaliści i oszuści każdego wyboru mają wpływ na sukces firmy, teraz lub w przyszłości. "It depends"(tm)

 0
Author: James Bradt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-03-03 23:56:01

2 podejścia:

  • dwie oddzielne transakcje. Jeśli transakcja 2 nie powiedzie się, transakcja 1 powinna zostać wycofana.
  • Karta To konto. Rejestruj transakcje na tym koncie. Jeśli obliczone saldo (zsumowanie wszystkich transakcji) osiągnie zero (lub mniej, nie powinno się zdarzyć), to karta jest "używana" - nie zapisuj jednak "używana" w DB. Po prostu czerp to z równowagi.
 0
Author: Chalky,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-22 23:33:25