Este aplicativo demonstra como executar operações atômicas de criação, atualização e exclusão em tabelas típicas de Clientes e Transações de Clientes em um relacionamento um-para-muitos, usando o Dapper micro-ORM e FluentMap e um padrão RepositórioUnidade de Trabalho. O banco de dados é composto por arquivos DBF e é acessado usando o driver Visual FoxPro OleDb. Não existem regras, gatilhos ou similares implementados no nível do banco de dados.
A título de exemplos de uso, alguns testes de integração usando XUnit são fornecidos no projeto 'Testes', e há também uma aplicação de console simples no projeto 'SimpleExample'.
Isso se baseia na abordagem detalhada por Ian Rufus nesta entrada do blog.
Importante! Este aplicativo requer o provedor Microsoft Visual FoxPro 9.0 OleDb. Este é um provedor apenas de 32 bits, não existe uma versão de 64 bits. Como resultado, este aplicativo deve ser compilado apenas para x86.
Dapper é um mapeador de objetos micro-ORM popular que estende IDbConnection com métodos convenientes que retornam resultados de banco de dados mapeados para tipos de entidade.
Aqui está uma tabela de banco de dados no 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
e aqui está a entidade C# que representa isso.
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 } " ;
}
}
O Dapper então oferece a capacidade de fazer coisas 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 ) ;
}
Observe a forma como os parâmetros de consulta são implementados - OleDB não suporta parâmetros nomeados, apenas parâmetros posicionais. Portanto, onde vários parâmetros são usados, a ordem é 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 é uma extensão Dapper que permite que o mapeamento entre as propriedades da entidade C# e os campos associados da tabela do banco de dados sejam declarados explicitamente.
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 ) ;
}
}
O padrão Unit Of Work permite que operações de criação, atualização e exclusão de banco de dados sejam executadas ou revertidas como uma única transação, permitindo a 'atomicidade' do banco de dados onde todas as atualizações ocorrem ou nenhuma ocorre.
Um padrão de repositório isola as operações do banco de dados da interface do usuário e permite que as operações do banco de dados sejam executadas adicionando, atualizando ou excluindo itens de uma coleção de objetos.
Existem duas classes de repositório no aplicativo, CustomerRepositoty
e CustomerTransactionRepository
. Cada um recebe um parâmetro do tipo IDbConnection por meio do construtor. A conexão de banco de dados a ser usada é então recuperada desse parâmetro:
private IDbConnection _connection { get => databaseTransaction . Connection ! ; }
Observe o perdão nulo '!' operador. É claro que essa injeção de dependência torna o provedor de banco de dados de classe independente. A classe então contém vários métodos para as operações CRUD do banco de dados, como o método a seguir que retornará uma Lista de objetos 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 ( ) ;
}
Nesta aplicação a unidade de trabalho é representada pela classe DapperUnitOfWork
. Esta é uma classe que implementa IDisposable . Possui instâncias de ambos os tipos de repositório. O construtor usa a string de conexão como parâmetro e configura o mapeamento do FluentMap, caso ainda não tenha sido feito. Em seguida, ele abre uma conexão OleDb e inicia uma nova transação.
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 ( ) ;
}
As propriedades do repositório na classe obtêm a transação do banco de dados injetada em seu getter, o código abaixo retornará um repositório existente ou criará um novo conforme necessário:
public CustomerRepository ? CustomerRepository
{
get
{
return customerRepository ??= new CustomerRepository ( dbTransaction ) ;
}
}
O método Commit()
na classe tenta confirmar a transação atual. Qualquer exceção causará uma reversão e a exceção será lançada. Há também um método Rollback()
que pode ser usado para reverter explicitamente a transação. Em todas as eventualidades, a transação atual será descartada e uma nova será criada, e os membros do repositório serão redefinidos.
public void Commit ( )
{
try
{
databaseTransaction . Commit ( ) ;
}
catch
{
databaseTransaction . Rollback ( ) ;
throw ;
}
finally
{
databaseTransaction . Dispose ( ) ;
databaseTransaction = databaseConnection . BeginTransaction ( ) ;
ResetRepositories ( ) ;
}
}
Como os objetos de repositório Customer
e CustomerTransaction
estão usando a mesma transação, a confirmação ou reversão são atômicos e representam uma unidade de trabalho.
Os métodos Commit()
e Rollback()
chamarão explicitamente o Dispose()
da classe. Este método cuida de descartar a transação e conexão atuais e redefinir os membros do repositório.
public void Dispose ( )
{
dbTransaction ? . Dispose ( ) ;
dbConnection ? . Dispose ( ) ;
GC . SuppressFinalize ( this ) ;
}
Importante Sempre descartar a transação e a conexão quando terminar é extremamente importante em um banco de dados baseado em arquivo como o DBF. Qualquer identificador de arquivo deixado aberto no arquivo do disco pode causar problemas para outros aplicativos./
Este é um exemplo simples - a unidade da classe de trabalho aqui é uma espécie de 'objeto divino', pois sempre deve conter uma instância de cada tipo de classe de repositório. Portanto, é um candidato para mais abstração.