31

Come si incapsula il salvataggio di più di un'entità in modo transazionale utilizzando il modello di repository? Ad esempio, cosa succede se volevo aggiungere un ordine e aggiornare lo stato del cliente in base alla creazione di questo ordine, ma lo faremo solo se l'ordine è stato completato correttamente? Tieni presente che, per questo esempio, gli ordini non sono una raccolta all'interno del cliente. Sono la loro stessa entità.Transazioni nel modello di deposito

Questo è solo un esempio forzato, quindi non mi interessa davvero se gli ordini dovrebbero o non dovrebbero essere all'interno dell'oggetto del cliente o anche nello stesso contesto limitato. Non mi interessa davvero quale sarà la tecnologia sottostante (ibernazione, EF, ADO.Net, Linq, ecc.). Voglio solo vedere come potrebbe apparire un certo codice chiamante in questo esempio evidentemente forzato di un'operazione tutto o niente.

risposta

7

Vorrei esaminare l'utilizzo di un tipo di sistema Transaction Scope/Context. Quindi potresti avere il seguente codice che è approssimativamente basato su .Net & C#.

public class OrderService 
{ 

public void CreateNewOrder(Order order, Customer customer) 
{ 
    //Set up our transactional boundary. 
    using (TransactionScope ts=new TransactionScope()) 
    { 
    IOrderRepository orderRepos=GetOrderRespository(); 
    orderRepos.SaveNew(order); 
    customer.Status=CustomerStatus.OrderPlaced; 

    ICustomerRepository customerRepository=GetCustomerRepository(); 
    customerRepository.Save(customer) 
    ts.Commit(); 
    } 
} 
} 

TransactionScope può nido Quindi diciamo che hai avuto un'azione che ha attraversato molteplici servizi l'applicazione creerebbe un TransactionScope pure. Ora nel .net corrente, se si utilizza TransactionScope, si rischia di inoltrarsi a un DTC, ma questo verrà risolto in futuro.

Avevamo creato la nostra classe TransactionScope che gestiva fondamentalmente le nostre connessioni DB e utilizzava transazioni SQL locali.

+0

Non penso che questa sia una soluzione in spirito di DDD. Fondamentalmente hai creato uno script di transazione che fa il lavoro di Domain Model. Il servizio non dovrebbe cambiare lo stato del cliente, ad esempio. –

+1

Qualcosa nel codice deve gestire questa regola aziendale, sia a questo livello che a un livello più alto il punto stava facendo le modifiche all'interno di un singolo TransactionScope consentendo transazioni locali o transazioni distribuite per gestire la transazione.Se la regola aziendale dice aggiornare il cliente ogni volta che viene effettuato un ordine, questo è un buon posto per gestirlo poiché tutti gli ordini passano qui. – JoshBerke

3

Si desidera esaminare l'implementazione dell'unità di modello di lavoro. Ci sono implementazioni là fuori per NHibernate. Uno è nel progetto Rhino Commons, c'è anche il Machine.UoW.

5

Utilizzando Spring.NET AOP + NHibernate è possibile scrivere la classe repository come normale e configurare le transazioni nel file XML personalizzato:

public class CustomerService : ICustomerService 
{ 
    private readonly ICustomerRepository _customerRepository; 
    private readonly IOrderRepository _orderRepository; 

    public CustomerService(
     ICustomerRepository customerRepository, 
     IOrderRepository orderRepository) 
    { 
     _customerRepository = customerRepository; 
     _orderRepository = orderRepository; 
    } 

    public int CreateOrder(Order o, Customer c) 
    { 
     // Do something with _customerRepository and _orderRepository 
    } 
} 

Nel file XML di selezionare i metodi che si desidera essere eseguito all'interno una transazione:

<object id="TxProxyConfigurationTemplate" 
      abstract="true" 
      type="Spring.Transaction.Interceptor.TransactionProxyFactoryObject, Spring.Data"> 

    <property name="PlatformTransactionManager" ref="HibernateTransactionManager"/> 

    <property name="TransactionAttributes"> 
     <name-values> 
     <add key="Create*" value="PROPAGATION_REQUIRED"/> 
     </name-values> 
    </property> 
    </object> 

    <object id="customerService" parent="TxProxyConfigurationTemplate"> 
    <property name="Target"> 
     <object type="MyNamespace.CustomerService, HibernateTest"> 
      <constructor-arg name="customerRepository" ref="customerRepository" /> 
      <constructor-arg name="orderRepository" ref="orderRepository" /> 
     </object> 
    </property> 

    </object> 

E nel codice si ottiene un'istanza della classe CustomerService in questo modo:

ICustomerService customerService = (ICustomerService)ContextRegistry 
    .GetContent() 
    .GetObject("customerService"); 

Spring.NET restituirà un proxy della classe CustomerService che applicherà una transazione quando si chiama il metodo CreateOrder. In questo modo non esiste alcun codice specifico per le transazioni all'interno delle tue classi di servizio. AOP si prende cura di esso. Per maggiori dettagli è possibile dare un'occhiata alla documentazione di Spring.NET.

2

Come faccio a incapsulare il risparmio di più di un'entità in modo transazionale utilizzando il modello repository ? Ad esempio, che cosa è se si desidera aggiungere un ordine e aggiornare lo stato del cliente in base a tale creazione dell'ordine, ma farlo solo se l'ordine è stato completato correttamente? Keep in attenzione che per questo esempio, gli ordini sono non una raccolta all'interno del cliente. Sono la loro propria entità.

Non è una responsabilità del repository, di solito è qualcosa fatto a un livello superiore.Sebbene tu abbia affermato di non essere interessato a tecnologie specifiche, ritengo che valga la pena di legare le soluzioni, ad esempio quando utilizzi NHibernate con un'app Web che probabilmente considereresti utilizzando session-per request.

Quindi, se è possibile gestire le transazioni a un livello superiore, allora le mie due opzioni sarebbero:

  1. controllo Upfront - Ad esempio in un servizio di coordinamento del comportamento decidere se si vuole procedere chiedendo l'Ordine/Cliente, se entrambi dicono di no, non provano nemmeno ad aggiornare nessuno di essi.
  2. Rollback - Basta procedere all'aggiornamento del cliente/ordine e se le cose non riescono parzialmente attraverso il rollback della transazione del database.

Se si sceglie la seconda opzione, la domanda è: cosa succede agli oggetti in memoria, il cliente potrebbe rimanere in uno stato incoerente. Se ciò è importante, e io lavoro in scenari in cui non è così come l'oggetto è stato caricato solo per quella richiesta, quindi sarei in considerazione il controllo in anticipo se è possibile perché è molto più facile rispetto alle alternative (riavvolgimento del in -memoria cambia o ricarica gli oggetti).

+1

Perché non è la responsabilità del deposito? L'intera idea non è di astrarre le operazioni di database dal modello di dominio? Per me, il repository è il posto migliore per mettere quel supporto transazionale. –

12

Stamattina, durante l'avvio del mio computer, ho affrontato il problema esatto per un progetto su cui sto lavorando. Ho avuto alcune idee che hanno portato al seguente design - e i commenti sarebbero stati più che fantastici. Sfortunatamente il design suggerito da Josh non è possibile, poiché devo lavorare con un server SQL remoto e non posso abilitare il servizio Distribute Transaction Coordinator su cui si basa.

La mia soluzione si basa su alcune modifiche ancora semplici al mio codice esistente.

In primo luogo, ho tutti i miei repository implementano una semplice interfaccia marcatore:

/// <summary> 
/// A base interface for all repositories to implement. 
/// </summary> 
public interface IRepository 
{ } 

In secondo luogo, ho lasciato tutti i miei operazione ha permesso repository implementare la seguente interfaccia:

/// <summary> 
/// Provides methods to enable transaction support. 
/// </summary> 
public interface IHasTransactions : IRepository 
{ 
    /// <summary> 
    /// Initiates a transaction scope. 
    /// </summary> 
    void BeginTransaction(); 

    /// <summary> 
    /// Executes the transaction. 
    /// </summary> 
    void CommitTransaction(); 
} 

L'idea è che in tutti i miei repository implemento questa interfaccia e aggiungo codice che introduce la transazione direttamente a seconda del fornitore effettivo (per i repository fasulli ho creato un elenco di delegati che viene eseguito su commit). Per LINQ to SQL che sarebbe stato facile fare implementazioni quali:

#region IHasTransactions Members 

public void BeginTransaction() 
{ 
    _db.Transaction = _db.Connection.BeginTransaction(); 
} 

public void CommitTransaction() 
{ 
    _db.Transaction.Commit(); 
} 

#endregion 

Questo naturalmente richiede che una nuova classe repository viene creato per ogni thread, ma questo è ragionevole per il mio progetto.

Ogni metodo che utilizza il repository deve richiamare lo BeginTransaction() e lo EndTransaction(), se il repository implementa IHasTransactions. Per rendere questa chiamata ancora più semplice, ho trovato le seguenti estensioni:

/// <summary> 
/// Extensions for spawning and subsequently executing a transaction. 
/// </summary> 
public static class TransactionExtensions 
{ 
    /// <summary> 
    /// Begins a transaction if the repository implements <see cref="IHasTransactions"/>. 
    /// </summary> 
    /// <param name="repository"></param> 
    public static void BeginTransaction(this IRepository repository) 
    { 
     var transactionSupport = repository as IHasTransactions; 
     if (transactionSupport != null) 
     { 
      transactionSupport.BeginTransaction(); 
     } 
    } 

    public static void CommitTransaction(this IRepository repository) 
    { 
     var transactionSupport = repository as IHasTransactions; 
     if (transactionSupport != null) 
     { 
      transactionSupport.CommitTransaction(); 
     } 
    } 
} 

I commenti sono apprezzati!

+0

Si potrebbe anche andare con una variante e creare un'istanza di repository per ogni transazione, inserirla in un'istruzione using e lasciare che Dispose() effettui il commit della transazione. Ciò farebbe astrarre la necessità di conoscere la transazione nel metodo del chiamante. –

+0

È piuttosto dolce. –

+0

Soluzione molto interessante ... –

Problemi correlati