Cette application montre comment effectuer des opérations atomiques de création, de mise à jour et de suppression sur des tables client et transaction client typiques dans une relation un-à-plusieurs, à l'aide du micro-ORM Dapper et de FluentMap et d'un modèle RéférentielUnité de travail. La base de données est composée de fichiers DBF et est accessible à l'aide du pilote Visual FoxPro OleDb. Aucune règle, déclencheur ou similaire n'est implémenté au niveau de la base de données.
À titre d'exemples d'utilisation, certains tests d'intégration utilisant XUnit sont fournis dans le projet 'Tests', et il existe également une application console simple dans le projet 'SimpleExample'.
Ceci est basé sur l'approche détaillée par Ian Rufus dans cet article de blog.
Important! Cette application nécessite le fournisseur Microsoft Visual FoxPro 9.0 OleDb. Il s'agit d'un fournisseur 32 bits uniquement, il n'existe pas de version 64 bits. Par conséquent, cette application doit être compilée uniquement pour x86.
Dapper est un mappeur d'objets micro-ORM populaire qui étend IDbConnection avec des méthodes pratiques qui renvoient les résultats de la base de données mappés aux types d'entités.
Voici une table de base de données au format 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
et voici l'entité C# qui le représente.
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 offre alors la possibilité de faire des choses comme :
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 ) ;
}
Notez la manière dont les paramètres de requête sont implémentés : OleDB ne prend pas en charge les paramètres nommés, uniquement les paramètres de position. Ainsi, lorsque plusieurs paramètres sont utilisés, l’ordre est 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 est une extension Dapper permettant de déclarer explicitement le mappage entre les propriétés d'entité C# et les champs de table de base de données associés.
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 ) ;
}
}
Le modèle d'unité de travail permet d'effectuer ou d'annuler les opérations de création, de mise à jour et de suppression de base de données en une seule transaction, ce qui permet une « atomicité » de la base de données là où toutes les mises à jour se produisent, ou aucune.
Un modèle de référentiel isole les opérations de base de données de l'interface utilisateur et permet d'effectuer des opérations de base de données en ajoutant, mettant à jour ou supprimant des éléments d'une collection d'objets.
Il existe deux classes de référentiel dans l'application, CustomerRepositoty
et CustomerTransactionRepository
. Chacun reçoit un paramètre de type IDbConnection via le constructeur. La connexion à la base de données à utiliser est ensuite récupérée à partir de ce paramètre :
private IDbConnection _connection { get => databaseTransaction . Connection ! ; }
Notez le « ! » qui pardonne les valeurs nulles. opérateur. Cette injection de dépendance rend bien entendu le fournisseur de base de données de classes indépendant. La classe contient ensuite diverses méthodes pour les opérations CRUD de la base de données, comme la méthode suivante qui renverra une liste d'objets Customer :
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 ( ) ;
}
Dans cette application, l'unité de travail est représentée par la classe DapperUnitOfWork
. Il s'agit d'une classe implémentant IDisposable . Il contient des instances des deux types de référentiel. Le constructeur prend la chaîne de connexion comme paramètre et configure le mappage FluentMap si ce n'est pas déjà fait. Il ouvre ensuite une connexion OleDb et démarre une nouvelle transaction.
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 ( ) ;
}
Les propriétés du référentiel sur la classe reçoivent la transaction de base de données injectée dans leur getter, le code ci-dessous renverra un référentiel existant ou en créera un nouveau selon les besoins :
public CustomerRepository ? CustomerRepository
{
get
{
return customerRepository ??= new CustomerRepository ( dbTransaction ) ;
}
}
La méthode Commit()
sur la classe tente de valider la transaction en cours. Toute exception entraînera une restauration et l'exception sera levée. Il existe également une méthode Rollback()
qui peut être utilisée pour annuler explicitement la transaction. Dans tous les cas, la transaction actuelle sera supprimée et une nouvelle créée, et les membres du référentiel seront réinitialisés.
public void Commit ( )
{
try
{
databaseTransaction . Commit ( ) ;
}
catch
{
databaseTransaction . Rollback ( ) ;
throw ;
}
finally
{
databaseTransaction . Dispose ( ) ;
databaseTransaction = databaseConnection . BeginTransaction ( ) ;
ResetRepositories ( ) ;
}
}
Étant donné que les objets du référentiel Customer
et CustomerTransaction
utilisent la même transaction, la validation ou l'annulation sont atomiques et représentent une unité de travail.
Les méthodes Commit()
et Rollback()
appelleront explicitement la méthode Dispose()
de la classe. Cette méthode prend en charge la suppression de la transaction et de la connexion en cours, ainsi que la réinitialisation des membres du référentiel.
public void Dispose ( )
{
dbTransaction ? . Dispose ( ) ;
dbConnection ? . Dispose ( ) ;
GC . SuppressFinalize ( this ) ;
}
Important Il est extrêmement important de toujours supprimer la transaction et la connexion une fois terminées dans une base de données basée sur des fichiers telle que DBF. Tout descripteur de fichier laissé ouvert sur le fichier disque peut entraîner des problèmes pour d'autres applications./
Ceci est un exemple simple : l'unité de classe de travail ici est une sorte d'« objet divin » puisqu'elle doit toujours contenir une instance de chaque type de classe de référentiel. C'est donc un candidat pour une abstraction plus poussée.