Wykonanie pojedynczej transakcji

0

Podzielilem aplikacje na moduly i w kazdym module utworzylem po jednym db context (jeden na scheme). Po utworzeniu klienta w module Customers chcialbym powiadomic inne moduly o tym zdarzeniu. ZTCW robi sie takie rzeczy za pomoca eventow, wiec utworzylem odpowiednie eventy i handlery. Tylko co, jesli operacja w innym module sie nie powiedzie? Chcialbym wykonac wszystkie operacje w jednej transakcji. Da sie to w ogole zrobic przy monolicie?

2

Oczywiście, że się da, to się nazywa transakcja rozproszona i jest znacnie starsze niż mikroserwisy.
Tylko ja bym się dwa razy zastanowił przed wejściem w to.

0

@somekind: Interesuje mnie najprostsze rozwiazanie.

0

Najprościej to bez transakcji rozproszonych. :P
A jakiż to proces w innym module może cofnąc tranksację i z jakiego powodu?

0

A co jesli nastapi awaria i dane nie zapisza sie w innym module? Prawdopodobienstwo male, ale czy nie nalezy sie przed tym chronic?

2

Ale jakie dane? Czy to na pewno powinien być oddzielny moduł, skoro te dane są niezbędne dla pierwszego modułu?

0

No np. klienci. Co jesli inne moduly beda mialy niepelne uproszczone bazy klientow?

1

Musisz się zastanowić przede wszystkim czy możesz w każdym z modułów wykonać akcję kompensacyjną czyli przywrócenie stanu wejściowego. Jeśli tak to możesz się zastanowić nad wykorzystaniem sagi. Jeśli nie to pewnie 2 phase commit będzie lepszy.
Obydwa podejścia jednak mocno komplikują cały system i lepiej 4 razy się zastanowić czy nie można tego w jakiś sposób uniknąć.

na pewno zastanowiłbym się nad architekturą

No np. klienci. Co jesli inne moduly beda mialy niepelne uproszczone bazy klientow?

a dlaczego mają mieć uproszczone bazy zamiast odpytać moduł klientów o dane jakich potrzebują?

0

No w DDD ta sama encja moze wystepowac w roznych bounded contextach w roznym charakterze. Wydaje mi sie, ze modul == bounded context. W sumie to nie wiedzialem, ze stosowanie DDD niesie za soba az takie problemy. O.o

2

@nobody01: a czy celem bounded contextu nie jest czasem sprawienie, żeby żadna encja poza niego nie wychodziła (czyli całkiem odwrotnie do tego co mówisz) ? ;)

0

@tdudzik: Tu masz przykład: https://4programmers.net/Forum/1608007

0

Przypomniałem sobie, że chyba @Aventus pisał kiedyś, że można osiągnąć transakcyjność, jeśli wyabstrahuje się repozytoria dla każdego modułu, a w implementacjach (znajdujących się np. w jakimś projekcie infrastruktury) będzie korzystać się z jednego db contextu. W artykule Microsoftu można znaleźć taki kod:

public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        // Dispatch Domain Events collection.
        // Choices:
        // A) Right BEFORE committing data (EF SaveChanges) into the DB. This makes
        // a single transaction including side effects from the domain event
        // handlers that are using the same DbContext with Scope lifetime
        // B) Right AFTER committing data (EF SaveChanges) into the DB. This makes
        // multiple transactions. You will need to handle eventual consistency and
        // compensatory actions in case of failures.        
        await _mediator.DispatchDomainEventsAsync(this);

        // After this line runs, all the changes (from the Command Handler and Domain
        // event handlers) performed through the DbContext will be committed
        var result = await base.SaveChangesAsync();
    }

A we właściwym repozytorium:

public static async Task DispatchDomainEventsAsync(this IMediator mediator, OrderingContext ctx)
        {
            var domainEntities = ctx.ChangeTracker
                .Entries<Entity>()
                .Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());

            var domainEvents = domainEntities
                .SelectMany(x => x.Entity.DomainEvents)
                .ToList();

            domainEntities.ToList()
                .ForEach(entity => entity.Entity.ClearDomainEvents());

            var tasks = domainEvents
                .Select(async (domainEvent) => {
                    await mediator.Publish(domainEvent);
                });

            await Task.WhenAll(tasks);
        }
0

@nobody01: co Ty nazywasz modułem?

0

@somekind: Np. jakiś podprojekt MyProject.Ordering odpowiadający logicznie jakiemuś bounded context.

0
nobody01 napisał(a):

Tu masz przykład: https://4programmers.net/Forum/1608007

W tym poście nie ma nic o przesyłaniu danych między bounded contextami.

0

@Kokoniłaj: Ten post był w odniesienia do pytania: czy celem bounded contextu nie jest czasem sprawienie, żeby żadna encja poza niego nie wychodziła?. W DDD często jest tak, że Aggregate Root (Root Entity) pojawia się w innych kontekstach w innej roli (w zalinkowanym poście jest do tego przykład). Pozostałe encje, niebędące Root Entity, faktycznie nie wychodzą poza bounded context. Także nie rozumiem, skąd te dwa plusy do tamtego pytania.

1

Ja tu widzę taki "problem"- rozbiłeś sobie aplikację na moduły/konteksty, z których każdy posiada własne dane. Typowy scenariusz w przypadku mikroserwisów. Zamiast bawić się w rozproszone transakcje na poziomie baz danych lepiej zastosować saga lub process manager. Na każde wydarzenie angażujące więcej niż jeden moduł/kontekst, będziesz posiadał wydarzenie kompensujące w przypadku niepowodzenia. Jeśli np. potwierdzenie zamówienia się nie powiedzie, wysłane zostanie polecenie cofnięcia zamówienia i wszystkie zainteresowane moduły zrobią co trzeba aby cofnąć zmiany dokonane wcześniej.

Ty natomiast nie masz mikroserwisów a monolit, i powinieneś to wykorzystać. Możesz skorzystać z jednej bazy danych (ale np. mieć oddzielną schema dla każdego modułu). Wspomniane przez Ciebie wyabstrahowanie repozytoriów jest bardzo proste- masz jeden kontekst ale każdy moduł operuje tylko na własnej abstrakcji repozytorium. Taki prosty przykład:

interface IProductsRepository
{
	IEnumerable<Product> FindProducts(string name);
}

interface IOrdersRepository
{
	Order GetOrder(string id);
}

class Context : DbContext, IProductsRepository, IOrdersRepository
{
	private DbSet<Product> Products { get; set; }
	private DbSet<Order> Orders { get; set; }

	public IEnumerable<Product> FindProducts(string name)
	{
		return Products.Where...
	}

	public Order GetOrder(string id)
	{
		return Orders.Find(id);
	}
}

Możesz również rozdzielić repozytoria do odrębnych klas i przekazywać im po prostu kontekst, ale to już kwestia preferencji.

2

Jeśli robisz modularny monolit, to tutaj zasady są takie same jak w mikroserwisach, zwykłe transakcje nie istnieją pomiędzy modułami i trzeba sobie radzić bez nich. Jimmy Bogard popełnił dwa cykle na ten temat:

Life Beyond Distributed Transactions: An Apostate's Implementation - A Primer
title

Refactoring Towards Resilience: A Primer

1 użytkowników online, w tym zalogowanych: 0, gości: 1