Garantire la coerenza transazionale con DDD


9

Sto iniziando con DDD e capisco che le radici aggregate vengono utilizzate per garantire la coerenza transnazionale. Non dovremmo modificare più aggregati in un servizio dell'applicazione.

Vorrei tuttavia sapere come affrontare la seguente situazione.

Ho una radice aggregata chiamata Products.

Esiste anche una radice aggregata chiamata Gruppo.

Entrambi hanno ID e possono essere modificati in modo indipendente.

Più prodotti possono puntare allo stesso gruppo.

Ho un servizio applicativo che può cambiare il gruppo di un prodotto:

ProductService.ChangeProductGroup(string productId, string groupId)

  1. Il gruppo di controllo esiste
  2. Ottieni il prodotto dal repository
  3. Imposta il suo gruppo
  4. Scrivi il prodotto nel repository

Ho anche un servizio applicativo in cui il gruppo può essere eliminato:

GroupService.DeleteGroup(string groupId) 1. Ottieni prodotti dal repository il cui groupId è impostato su groupId fornito, assicurati che il conteggio sia 0 o annulla 2. Elimina il gruppo dal repository dei gruppi 3. Salva le modifiche

La mia domanda è il seguente scenario, cosa accadrebbe se:

Nel ProductService.ChangeProductGroup, controlliamo che il gruppo esista (lo fa), quindi subito dopo questo controllo un utente separato elimina il productGroup (tramite l'altro GroupService.DeleteGroup). In questo caso abbiamo impostato un riferimento a un prodotto che è stato appena eliminato?

È un difetto nel mio progetto il fatto che dovrei usare un design di dominio diverso (aggiungendo elementi aggiuntivi se necessario) o dovrei usare le transazioni?

Risposte:


2

Abbiamo lo stesso problema.

E non vedo alcun modo per risolvere questo problema, ma con le transazioni o con il controllo di coerenza nel DB per ottenere un'eccezione nel caso peggiore.

È inoltre possibile utilizzare il blocco pessimistico, bloccando la radice aggregata o solo le sue parti per altri clienti fino al completamento della transazione commerciale, che è in una certa misura equivalente alla transazione serializzabile.

Il modo in cui vai pesantemente dipende dal tuo sistema e dalla tua logica aziendale.
La concorrenza non è affatto un compito facile.
Anche se riesci a rilevare un problema, come lo risolverai? Annullare semplicemente l'operazione o consentire all'utente di "unire" le modifiche?

Usiamo Entity Framework ed EF6 usa read_commited_snapshot per impostazione predefinita, quindi due letture consecutive dal repository possono darci dati incoerenti. Lo teniamo a mente per il futuro, quando i processi aziendali saranno delineati in modo più chiaro e possiamo prendere una decisione informata. E sì, controlliamo comunque la coerenza a livello di modello. Questo almeno consente di testare BL separatamente dal DB.

Ti suggerisco anche di riflettere sulla coerenza del repository nel caso in cui tu abbia transazioni commerciali "lunghe". È piuttosto complicato come si è scoperto.


1

Penso che questa non sia una domanda specifica di progettazione guidata dal dominio, ma una gestione delle risorse.

Le risorse devono semplicemente essere bloccate man mano che vengono acquisite .

Questo è vero quando il servizio applicativo sa in anticipo che la risorsa acquisita (l' Groupaggregato, nel tuo esempio) sta per essere modificata. In Domain-Driven Design, il blocco delle risorse è un aspetto che appartiene al repository.


No, il blocco non è un "must". In realtà è una cosa piuttosto orribile da fare con transazioni di lunga durata. Stai meglio usando la concorrenza ottimistica, che ogni moderno ORM supporta immediatamente. E il bello della concorrenza ottimistica è che funziona anche con database non transazionali, purché supportino gli aggiornamenti atomici.
Aaronaught il

1
Concordo sugli svantaggi del blocco all'interno di transazioni a lungo termine, ma lo scenario descritto da @ g18c suggerisce piuttosto il contrario, ovvero brevi operazioni su singoli aggregati.
Rucamzu,

0

Perché non memorizzi gli ID prodotto nell'entità Gruppo? In questo modo hai a che fare con un solo aggregato che renderà le cose più facili.

Sarà quindi necessario implementare un tipo di modello di concorrenza, ad es. Se si sceglie la concorrenza ottimistica, è sufficiente aggiungere una proprietà versione all'entità Gruppo e generare un'eccezione se le versioni non corrispondono durante l'aggiornamento, ad es.

Aggiungi prodotto a un gruppo

  1. Verifica se il prodotto esiste
  2. Ottieni gruppo dal repository (inclusa la sua proprietà id versione)
  3. Group.Add (ProductId)
  4. Salva gruppo utilizzando il repository (aggiorna con un nuovo ID versione e aggiungi una clausola where per garantire che la versione non sia stata modificata.)

Molti ORM hanno una concorrenza ottimistica integrata utilizzando gli ID di versione o i timestamp ma è facile creare il proprio. Ecco un buon post su come è stato fatto in Nhibernate http://ayende.com/blog/3946/nhibernate-mapping-concurrency .


0

Sebbene le transazioni possano essere di aiuto, esiste un altro modo, soprattutto se lo scenario è limitato a pochi casi.

È possibile includere controlli nelle query che eseguono le mutazioni, rendendo effettivamente ciascuna coppia check-and-mutate un'operazione atomica.

La query di aggiornamento del prodotto interna si unisce al gruppo (di recente riferimento). Questo non gli fa aggiornare nulla se quel gruppo non c'è più.

La query di eliminazione di gruppo unisce tutti i prodotti che puntano ad esso e ha la condizione aggiuntiva che (qualsiasi colonna di) il risultato del join debba essere nullo. Questo fa sì che non elimini nulla se alcuni prodotti lo indicano.

Le query restituiscono il numero di righe corrispondenti o aggiornate. Se il risultato è 0, hai perso una condizione di gara e non hai fatto nulla. Può generare un'eccezione e il livello applicazione può gestirlo come desidera, ad esempio riprovando dall'inizio.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.