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?
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.
@somekind: Interesuje mnie najprostsze rozwiazanie.
Najprościej to bez transakcji rozproszonych. :P
A jakiż to proces w innym module może cofnąc tranksację i z jakiego powodu?
A co jesli nastapi awaria i dane nie zapisza sie w innym module? Prawdopodobienstwo male, ale czy nie nalezy sie przed tym chronic?
Ale jakie dane? Czy to na pewno powinien być oddzielny moduł, skoro te dane są niezbędne dla pierwszego modułu?
No np. klienci. Co jesli inne moduly beda mialy niepelne uproszczone bazy klientow?
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ą?
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
@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) ? ;)
@tdudzik: Tu masz przykład: https://4programmers.net/Forum/1608007
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);
}
@nobody01: co Ty nazywasz modułem?
@somekind: Np. jakiś podprojekt MyProject.Ordering
odpowiadający logicznie jakiemuś bounded context.
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.
@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.
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.
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