Modèle de référentiel et de mappeur de données
Après avoir beaucoup lu sur le référentiel et le Data Mapper, j'ai décidé d'implémenter ces modèles dans un projet de test. Puisque je suis nouveau à ceux-ci, j'aimerais avoir votre point de vue sur la façon dont j'ai implémenté ceux-ci dans un projet simple.
Jeremy Miller dit:
Faites une sorte de projet de codage personnel non trivial où vous pouvez expérimenter librement avec des modèles de conception.
Mais je ne sais pas que j'ai fait toutes ces choses correctement ou pas.
Voici la structure de mon projet :
Comme vous pouvez le voir, il y a beaucoup de dossiers que je vais décrire en détail ci-dessous.
-
Domaine: Les entités du domaine du projet vont ici j'ai une classe de Personnel simple qui est héritée de la classe EntityBase, la classe EntityBase a une seule propriété nommée Id.
public int Id { get; set; }
-
Infrustructure: Voici une couche D'accès aux données simple avec deux classes. SqlDataLayer est une classe simple héritée d'une classe abstraite nommée DataLayer. Ici, je fournis des fonctionnalités comme le code suivant:
public SQLDataLayer() { const string connString = "ConnectionString goes here"; _connection = new SqlConnection(connString); _command = _connection.CreateCommand(); }
Ajout d'un paramètre à la collection de paramètres de commandes:
public override void AddParameter(string key, string value) {
var parameter = _command.CreateParameter();
parameter.Value = value;
parameter.ParameterName = key;
_command.Parameters.Add(parameter);
}
Exécution De DataReader:
public override IDataReader ExecuteReader() {
if (_connection.State == ConnectionState.Closed)
_connection.Open();
return _command.ExecuteReader();
}
Et ainsi de suite.
- Repository: ici, j'ai essayé d'implémenter le modèle de référentiel. IRepository est une interface générique
IRepository.cs:
public interface IRepository<TEntity> where TEntity : EntityBase
{
DataLayer Context { get; }
TEntity FindOne(int id);
ICollection<TEntity> FindAll();
void Delete(TEntity entity);
void Insert(TEntity entity);
void Update(TEntity entity);
}
Référentiel.cs:
public class Repository<TEntity> : IRepository<TEntity> where TEntity : EntityBase, new() {
private readonly DataLayer _domainContext;
private readonly DataMapper<TEntity> _dataMapper;
public Repository(DataLayer domainContext, DataMapper<TEntity> dataMapper) {
_domainContext = domainContext;
_dataMapper = dataMapper;
}
public DataLayer Context {
get { return _domainContext; }
}
public TEntity FindOne(int id)
{
var commandText = AutoCommand.CommandTextBuilder<TEntity>(CommandType.StoredProcedure, MethodType.FindOne);
// Initialize parameter and their types
Context.AddParameter("Id", id.ToString(CultureInfo.InvariantCulture));
Context.SetCommandType(CommandType.StoredProcedure);
Context.SetCommandText(commandText);
var dbReader = Context.ExecuteReader();
return dbReader.Read() ? _dataMapper.Map(dbReader) : null;
}
Je n'ai pas exposé les méthodes non implémentées de IRepository.
Ici, dans la classe de référentiel Générique, j'attends deux paramètres dans le constructeur, d'abord une référence à ma classe SqlDataLayer et deuxièmement une référence à Entity DataMapper. Ces paramètres envoyés par chaque classe de référentiel D'entités héritées de la classe de référentiel. par exemple :
public class PersonnelRepository : Repository<Personnel>, IPersonnelRepository {
public PersonnelRepository(DataLayer domainContext, PersonnelDataMapper dataMapper)
: base(domainContext, dataMapper) {
}
}
Comme vous pouvez le voir ici dans la méthode FindOne, j'ai essayé d'automatiser certaines opérations telles que la création de CommandText, puis j'ai profité de ma classe DataLayer pour configurer la commande et enfin exécuter la commande pour obtenir IDataReader. Je passe IDataReader à ma classe DataMapper pour mapper à L'entité.
-
DomainMapper: enfin ici, je mappe le résultat D'IDataReader aux entités, ci-dessous est un exemple de la façon dont je mappe L'entité du Personnel:
public class PersonnelDataMapper : DataMapper<Personnel> { public override Personnel Map(IDataRecord record) { return new Personnel { FirstName = record["FirstName"].ToString(), LastName = record["LastName"].ToString(), Address = record["Address"].ToString(), Id = Convert.ToInt32(record["Id"]) }; }}
Utilisation :
using (var context = new SQLDataLayer()) {
_personnelRepository = new PersonnelRepository(context, new PersonnelDataMapper());
var personnel = _personnelRepository.FindOne(1);
}
Je sais que j'ai fait beaucoup d'erreurs ici, c'est pourquoi je suis ici. J'ai besoin de vos conseils pour savoir ce que j'ai mal fait ou quels sont les bons points dans ce projet de test simple.
Merci préalablement.
1 réponses
Quelques points:
Il me semble que, dans l'ensemble, vous avez une bonne conception. C'est démontré, en partie, par le fait que vous pouvez y apporter des modifications avec peu d'impact sur toutes les classes en dehors de celles qui sont modifiées (couplage faible). Cela dit, C'est très proche de ce que fait Entity Framework, donc bien que ce soit un bon projet personnel, j'envisagerais D'utiliser EF avant de l'implémenter dans un projet de production.
Votre classe DataMapper pourrait être rendue Générique (disons,
GenericDataMapper<T>
) en utilisant la réflexion. itérer sur les propriétés de type T en utilisant reflection , et les obtenir dynamiquement à partir de la ligne de données.En supposant que vous créez un DataMapper Générique, vous pouvez envisager de créer une méthode
CreateRepository<T>()
sur DataLayer, afin que les utilisateurs n'aient pas besoin de s'inquiéter des détails du type de mappeur à choisir.Une critique mineure-vous supposez que toutes les entités auront un seul ID entier nommé "Id", et qu'une procédure stockée sera configurée pour les récupérer par tel. Vous pourrez peut-être améliorer votre conception ici en permettant des clés primaires de types différents, encore une fois peut-être en utilisant des génériques.
Vous ne voulez probablement pas réutiliser les objets de connexion et de commande comme vous le faites. Ce n'est pas thread safe, et même si c'était le cas, vous vous retrouveriez avec des conditions de course surprenantes et difficiles à déboguer autour des Transactions DB. Vous devez soit créer de nouveaux objets de connexion et de commande pour chaque appel de fonction (en vous assurant de disposer de après que vous avez terminé), ou implémenter une synchronisation autour des méthodes qui accèdent à la base de données.
Par exemple, je suggère cette version alternative de ExecuteReader:
public override IDataReader ExecuteReader(Command command) {
var connection = new SqlConnection(connString);
command.Connection = connection;
return command.ExecuteReader();
}
Votre ancien a réutilisé l'objet command, ce qui pourrait conduire à des conditions de course entre les appelants multithread. Vous souhaitez également créer une nouvelle connexion, car l'ancienne connexion peut être engagée dans une transaction démarrée par un appelant différent. Si vous souhaitez réutiliser des opérations, vous devrait créer une connexion, commencer une transaction et réutiliser cette transaction jusqu'à ce que vous ayez exécuté toutes les commandes que vous souhaitez associer à la transaction. Par exemple, vous pouvez créer des surcharges de vos méthodes ExecuteXXX comme ceci:
public override IDataReader ExecuteReader(Command command, ref SqlTransaction transaction) {
SqlConnection connection = null;
if (transaction == null) {
connection = new SqlConnection(connString);
transaction = connection.BeginTransaction();
} else {
connection = transaction.Connection;
}
command.Connection = connection;
return command.ExecuteReader();
}
// When you call this, you can pass along a transaction by reference. If it is null, a new one will be created for you, and returned via the ref parameter for re-use in your next call:
SqlTransaction transaction = null;
// This line sets up the transaction and executes the first command
var myFirstReader = mySqlDataLayer.ExecuteReader(someCommandObject, ref transaction);
// This next line gets executed on the same transaction as the previous one.
var myOtherReader = mySqlDataLayer.ExecuteReader(someOtherCommandObject, ref transaction);
// Be sure to commit the transaction afterward!
transaction.Commit();
// Be a good kid and clean up after yourself
transaction.Connection.Dispose();
transaction.Dispose();
- Last but not least, après avoir travaillé avec Jeremy, je suis sûr qu'il dirait que vous devriez avoir des tests unitaires pour toutes ces classes!