31

¿Cómo encapsulo el ahorro de más de una entidad de forma transaccional utilizando el patrón de repositorio? Por ejemplo, ¿qué sucede si deseo agregar un pedido y actualizar el estado del cliente en función de la creación de ese pedido, pero solo lo hago si el pedido se completó correctamente? Tenga en cuenta que para este ejemplo, los pedidos no son una colección dentro del cliente. Ellos son su propia entidad.Transacciones en el patrón de repositorio

Esto es solo un ejemplo artificial, por lo que realmente no me importa si los pedidos deben o no estar dentro del objeto del cliente o incluso en el mismo contexto delimitado. Realmente no me importa qué tecnología subyacente se utilizará (nHibernate, EF, ADO.Net, Linq, etc.) Solo quiero ver cómo se vería un código de llamada en este ejemplo claramente inventado de una operación de todo o nada.

Respuesta

7

Me gustaría ver el uso de algún tipo de Transaction Scope/Context system. Por lo tanto, es posible que tenga el siguiente código, que se basa en .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 puede nido así que vamos a decir que tenía una acción que cruzó múltiples servicios que su aplicación crearía un TransactionScope también. Ahora en el .net actual, si usa TransactionScope, corre el riesgo de escalar a un DTC, pero esto se resolverá en el futuro.

Creamos nuestra propia clase TransactionScope que básicamente gestionaba nuestras conexiones de base de datos y utilizaba transacciones SQL locales.

+0

No creo que esta sea una solución en espíritu de DDD. Básicamente, ha creado un script de transacción que hace el trabajo del Modelo de Dominio. El servicio no debería cambiar el estado del cliente, por ejemplo. –

+1

Algo en el código tiene que manejar esta regla comercial, ya sea en este nivel o en un nivel más alto, el punto era hacer los cambios dentro de un único TransactionScope que permite transacciones locales o transacciones distribuidas para manejar la transacción.Si la regla comercial dice actualizar al cliente cada vez que se realiza un pedido, este es un buen lugar para manejarlo ya que todos los pedidos se procesan aquí. – JoshBerke

3

Desea ver la implementación del patrón de la unidad de trabajo. Hay implementaciones para NHibernate. Uno está en el proyecto de Rhino Commons, también está el Machine.UoW.

5

Usando Spring.NET AOP + NHibernate puede escribir su clase repositorio como normal y configurar sus transacciones en el archivo XML personalizado:

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 
    } 
} 

En el archivo XML permite seleccionar los métodos que le gustaría ser ejecutado en el interior una transacción:

<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> 

Y en su código que obtener una instancia de la clase a Cliente así:

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

Spring.NET le devolverá un proxy de la clase CustomerService que aplicará una transacción cuando llame al método CreateOrder. De esta forma, no hay código específico de transacción dentro de sus clases de servicio. AOP se ocupa de eso. Para más detalles, puede echar un vistazo a la documentación de Spring.NET.

2

¿Cómo encapsular el ahorro de más de una entidad de una manera transaccional utilizando el patrón repositorio ? Por ejemplo, ¿qué si quería agregar un pedido y actualizar el estado del cliente basado en esa creación de orden , pero solo hacerlo si el pedido se completó correctamente? Mantenga en tenga en cuenta que para este ejemplo, los pedidos son no una colección dentro del cliente. Ellos son su propia entidad.

No es una responsabilidad del repositorio, generalmente es algo que se hace en un nivel superior.Aunque dijiste que no te interesaban las tecnologías específicas, creo que vale la pena atar las soluciones, por ejemplo, cuando utilizas NHibernate con una aplicación web, probablemente considerarías usar session-per request.

Así que si usted puede manejar transacciones a un nivel más alto que mis dos opciones serían:

  1. cheque Upfront - Por ejemplo, en un servicio de coordinación del comportamiento de decidir si desea continuar preguntando el pedido/cliente, si dicen que no lo hacen, ni siquiera intentan actualizar ninguno de ellos.
  2. Rollback - Simplemente continúe actualizando el Cliente/Pedido y si las cosas fallan durante la reversión de la transacción de la base de datos.

Si va por la segunda opción, entonces la pregunta es qué ocurre con los objetos en la memoria, su cliente puede quedar en un estado incoherente. Si eso es importante, y trabajo en escenarios en los que no lo hace, ya que el objeto solo se cargó para esa solicitud, entonces consideraría la verificación inicial si es posible porque es mucho más fácil que las alternativas (revertir el -memory cambia o recarga los objetos).

+1

¿Por qué no es responsabilidad del repositorio? ¿No es la idea general de abstraer las operaciones de la base de datos del modelo de dominio? Para mí, el repositorio es el mejor lugar para poner ese soporte transaccional. –

12

Al arrancar mi computadora esta mañana me enfrenté al problema exacto de un proyecto en el que estoy trabajando. Tenía algunas ideas que me llevan al siguiente diseño, y los comentarios serían más que increíbles. Desafortunadamente, el diseño sugerido por Josh no es posible, ya que tengo que trabajar con un servidor SQL remoto y no puedo habilitar el servicio Distribuir coordinador de transacciones en el que confía.

Mi solución se basa en unos pocos cambios simples a mi código existente.

En primer lugar, tengo todos mis repositorios implementar una sencilla interfaz de marcador:

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

En segundo lugar, dejar que todos mis repositorios habilitados transacciones implementan la interfaz siguiente:

/// <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(); 
} 

La idea es que en todos mis repositorios Implemento esta interfaz y agrego un código que introduce la transacción directamente dependiendo del proveedor real (para repositorios falsos he hecho una lista de delegados que se ejecuta en la confirmación). Para LINQ to SQL sería fácil de hacer implementaciones tales como:

#region IHasTransactions Members 

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

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

#endregion 

Por supuesto, esto requiere que se crea una nueva clase de repositorio para cada hilo, pero esto es razonable para mi proyecto.

Cada método que utiliza el repositorio necesita invocar el BeginTransaction() y el EndTransaction(), si el repositorio implementa IHasTransactions. Para hacer esta llamada aún más fácil, se me ocurrieron las siguientes extensiones:

/// <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(); 
     } 
    } 
} 

¡Los comentarios son apreciados!

+0

También podría ir con una variante y crear una instancia de repositorio para cada transacción, ponerla dentro de una declaración using y dejar que Dispose() confirme la transacción. Esto abstraería la necesidad de saber acerca de la transacción en el método del que llama. –

+0

Eso es muy dulce. –

+0

Solución muy interesante ... –

Cuestiones relacionadas