Esta aplicación demuestra cómo realizar operaciones atómicas de creación, actualización y eliminación en tablas típicas de Clientes y Transacciones de Clientes en una relación de uno a muchos, utilizando Dapper micro-ORM y FluentMap y un patrón RepositorioUnidad de Trabajo. La base de datos se compone de archivos DBF y se accede a ella mediante el controlador OleDb de Visual FoxPro. No existen reglas, activadores o similares implementados a nivel de base de datos.
A modo de ejemplos de uso, se proporcionan algunas pruebas de integración usando XUnit en el proyecto 'Pruebas', y también hay una aplicación de consola simple en el proyecto 'SimpleExample'.
Esto se basa en el enfoque detallado por Ian Rufus en esta entrada de blog.
¡Importante! Esta aplicación requiere el proveedor OleDb de Microsoft Visual FoxPro 9.0. Este es un proveedor exclusivo de 32 bits, no existe una versión de 64 bits. Como resultado, esta aplicación debe compilarse únicamente para x86.
Dapper es un popular mapeador de objetos micro-ORM que extiende IDbConnection con métodos convenientes que devuelven resultados de bases de datos asignados a tipos de entidades.
Aquí hay una tabla de base de datos en formato DBF:
Field Field Name Type Width
1 CU_CODE Character 10
2 CU_NAME Character 50
3 CU_ADDR1 Character 50
4 CU_ADDR2 Character 50
5 CU_POSTCODE Character 10
6 CU_BALANCE Numeric 12
y aquí está la entidad C# que lo representa.
public class Customer
{
[ Key ]
public string Code { get ; set ; } = string . Empty ;
public string Name { get ; set ; } = string . Empty ;
public string Address1 { get ; set ; } = string . Empty ;
public string ? Address2 { get ; set ; } = string . Empty ;
public string ? Postcode { get ; set ; }
///
/// Gets or sets the customer balance. Not writable. It can only
/// be updated by inserting, deleting or updating a
/// transaction or transactions for this customer.
///
[ Write ( false ) ]
public float Balance { get ; set ; }
public override string ToString ( )
{
return $ " { Code } { Name } " ;
}
}
Dapper luego brinda la capacidad de hacer cosas como:
public Customer GetByCode ( string code )
{
var cmd = @"select cu_code, cu_name, cu_addr1, cu_addr2, cu_postcode, cu_balance " ;
cmd += "from Customers where cu_code = ?" ;
return _connection . QueryFirstOrDefault < Customer > ( cmd , param : new { c = code } , transaction ) ;
}
Tenga en cuenta la forma en que se implementan los parámetros de consulta: OleDB no admite parámetros con nombre, solo parámetros posicionales. Entonces, cuando se utilizan múltiples parámetros, el orden es vital:
public void Update ( Customer customer )
{
var cmd = @"update Customers set cu_name=?, cu_addr1=?, cu_addr2=?, cu_postcode=? where cu_code=?" ;
_connection . ExecuteScalar ( cmd , param : new
{
n = customer . Name ,
add1 = customer . Address1 ,
add2 = customer . Address2 ,
pc = customer . Postcode ,
acc = customer . Code ,
} ,
transaction ) ;
}
FluentMap es una extensión de Dapper que permite declarar explícitamente el mapeo entre las propiedades de la entidad C# y los campos de la tabla de la base de datos asociada.
public class CustomerEntityMap : EntityMap < Customer >
{
public CustomerEntityMap ( )
{
Map ( c => c . Code ) . ToColumn ( "cu_code" , caseSensitive : false ) ;
Map ( c => c . Name ) . ToColumn ( "cu_name" , caseSensitive : false ) ;
Map ( c => c . Address1 ) . ToColumn ( "cu_addr1" , caseSensitive : false ) ;
Map ( c => c . Address2 ) . ToColumn ( "cu_addr2" , caseSensitive : false ) ;
Map ( c => c . Postcode ) . ToColumn ( "cu_postcode" , caseSensitive : false ) ;
Map ( c => c . Balance ) . ToColumn ( "cu_balance" , caseSensitive : false ) ;
}
}
El patrón Unidad de trabajo permite que las operaciones de creación, actualización y eliminación de bases de datos se realicen o reviertan como una única transacción, lo que permite la "atomicidad" de la base de datos donde se producen todas las actualizaciones o ninguna.
Un patrón de repositorio aísla las operaciones de la base de datos de la interfaz de usuario y permite que las operaciones de la base de datos se realicen agregando, actualizando o eliminando elementos de una colección de objetos.
Hay dos clases de repositorio en la aplicación, CustomerRepositoty
y CustomerTransactionRepository
. A cada uno se le pasa un parámetro de tipo IDbConnection a través del constructor. La conexión de base de datos que se utilizará se recupera de ese parámetro:
private IDbConnection _connection { get => databaseTransaction . Connection ! ; }
Tenga en cuenta el '!' que perdona nulos operador. Esta inyección de dependencia, por supuesto, hace que el proveedor de la base de datos de clase sea independiente. Luego, la clase contiene varios métodos para las operaciones CRUD de la base de datos, como el siguiente método que devolverá una Lista de objetos de Cliente:
public List < Customer > GetAll ( )
{
var cmd = @"select cu_code, cu_name, cu_addr1, cu_addr2, cu_postcode, cu_balance " ;
cmd += "from Customers " ;
return _connection . Query < Customer > ( cmd , transaction : transaction ) . ToList ( ) ;
}
En esta aplicación, la unidad de trabajo está representada por la clase DapperUnitOfWork
. Esta es una clase que implementa IDisposable . Tiene instancias de ambos tipos de repositorio. El constructor toma la cadena de conexión como parámetro y configura el mapeo de FluentMap si aún no lo ha hecho. Luego abre una conexión OleDb e inicia una nueva transacción.
public class DapperUnitOfWork : IDisposable
{
private readonly IDbConnection databaseConnection ;
private IDbTransaction databaseTransaction ;
///
/// Initializes a new instance of the class.
/// Sets up the unit of work and configures the FluentMap mappings.
/// Opens the OleDb connection.
///
/// The OleDb connection string.
public DapperUnitOfWork ( string connString )
{
ConnectionString = connString ;
if ( ! FluentMapper . EntityMaps . Any ( m => m . Key == typeof ( Entities . Customer ) ) )
{
FluentMapper . Initialize ( config =>
{
config . AddMap ( new CustomerEntityMap ( ) ) ;
config . AddMap ( new CustomerTransactionEntityMap ( ) ) ;
} ) ;
}
databaseConnection = new OleDbConnection ( ConnectionString ) ;
databaseConnection . Open ( ) ;
// Some default setup items for the connection.
// 'Set null off' - any inserts will insert the relevant empty value for the database field type instead of a null
// where a value is not supplied.
// 'set exclusive off' - tables will be opened in shared mode.
// 'set deleted on' - unintuitively this means that table rows marked as deleted will be ignored in SELECTs.
var cmd = $ "set null off { Environment . NewLine } set exclusive off { Environment . NewLine } set deleted on { Environment . NewLine } " ;
databaseConnection . Execute ( cmd ) ;
databaseTransaction = databaseConnection . BeginTransaction ( ) ;
}
Las propiedades del repositorio en la clase obtienen la transacción de la base de datos inyectada en su captador, el siguiente código devolverá un repositorio existente o creará uno nuevo según sea necesario:
public CustomerRepository ? CustomerRepository
{
get
{
return customerRepository ??= new CustomerRepository ( dbTransaction ) ;
}
}
El método Commit()
de la clase intenta confirmar la transacción actual. Cualquier excepción provocará una reversión y se generará la excepción. También existe un método Rollback()
que se puede utilizar para revertir explícitamente la transacción. En todos los casos, se eliminará la transacción actual, se creará una nueva y se restablecerán los miembros del repositorio.
public void Commit ( )
{
try
{
databaseTransaction . Commit ( ) ;
}
catch
{
databaseTransaction . Rollback ( ) ;
throw ;
}
finally
{
databaseTransaction . Dispose ( ) ;
databaseTransaction = databaseConnection . BeginTransaction ( ) ;
ResetRepositories ( ) ;
}
}
Debido a que los objetos del repositorio Customer
y CustomerTransaction
utilizan la misma transacción, la confirmación o la reversión son atómicas y representan una unidad de trabajo.
Tanto el método Commit()
como Rollback()
llamarán explícitamente al método Dispose()
de la clase. Este método se encarga de deshacerse de la transacción y la conexión actuales y de restablecer los miembros del repositorio.
public void Dispose ( )
{
dbTransaction ? . Dispose ( ) ;
dbConnection ? . Dispose ( ) ;
GC . SuppressFinalize ( this ) ;
}
Importante Desechar siempre la transacción y la conexión cuando finalice es extremadamente importante en una base de datos basada en archivos como DBF. Cualquier identificador de archivo que se deje abierto en el archivo del disco puede causar problemas para otras aplicaciones./
Este es un ejemplo simple: la unidad de clase de trabajo aquí es una especie de 'objeto divino' ya que siempre tiene que contener una instancia de cada tipo de clase de repositorio. Por tanto, es candidato a una mayor abstracción.