web-dev-qa-db-fra.com

Modèle de référentiel et de mappeur de données

Après de nombreuses lectures sur le référentiel et le mappeur de données, j'ai décidé d'implémenter ces modèles dans un projet de test. Étant donné que je suis nouveau dans ces domaines, j'aimerais avoir votre avis sur la façon dont je les ai mis en œuvre dans un projet simple.

Jeremy Miller dit:

Faites une sorte de projet de codage personnel non trivial où vous pouvez librement expérimenter avec des modèles de conception.

Mais je ne sais pas si j'ai bien fait toutes ces choses ou pas.

Voici ma structure de projet:

enter image description here

Comme vous pouvez le voir, il existe de nombreux dossiers que je vais décrire en détail ci-dessous.

  • Domaine: les entités de domaine de projet vont ici J'ai une classe Personnel simple 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 simple couche d'accès aux données avec deux classes. SqlDataLayer est une classe simple héritée d'une classe abstraite nommée DataLayer. Ici, je fournis quelques fonctionnalités comme le code suivant:

    public SQLDataLayer() {
        const string connString = "ConnectionString goes here";
        _connection = new SqlConnection(connString);
        _command = _connection.CreateCommand();
    }
    

ajout de paramètres à 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();
    }

etc.

  • Référentiel: Ici, j'ai essayé d'implémenter un 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);
}

Repository.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 d'IRepository.

Ici, dans la classe Generic Repository, je m'attends à ce que deux paramètres dans le constructeur soient 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 Entities Repository héritée de la classe Repository. 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 le mapper à l'entité.

  • DomainMapper: Enfin, ici, je mappe le résultat d'IDataReader aux entités, ci-dessous un exemple de la façon dont je mappe l'entité 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"])
        };
    }}
    

tilisation:

    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 fait de mal ou quels sont les bons points de ce projet de test simple.

Merci d'avance.

39
saber

Quelques points:

  1. Cela me frappe que dans l'ensemble, vous avez un bon design. Cela 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 même s'il s'agit d'un bon projet personnel, je considérerais d'utiliser EF avant de l'implémenter dans un projet de production.

  2. 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 la réflexion , et les extraire dynamiquement de la ligne de données.

  3. 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 à se soucier des détails du type de mappeur à choisir.

  4. Une critique mineure - vous supposez que toutes les entités auront un ID entier unique nommé "Id", et qu'une procédure stockée sera configurée pour les récupérer par ce biais. Vous pourrez peut-être améliorer votre conception ici en permettant des clés primaires de types différents, peut-être encore en utilisant des génériques.

  5. Vous ne voudrez probablement pas réutiliser les objets Connexion et Commande comme vous le faites. Ce n'est pas sûr pour les threads, et même si c'était le cas, vous vous retrouveriez avec des conditions de concurrence surprenantes et difficiles à déboguer autour des transactions DB. Vous devez soit créer de nouveaux objets Connexion et Commande pour chaque appel de fonction (en vous assurant de les supprimer une fois que vous avez terminé), soit implémenter une synchronisation autour des méthodes qui accèdent à la base de données.

Par exemple, je suggère cette version alternative d'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 de commande, ce qui pourrait entraîner des conditions de concurrence entre les appelants multithreads. Vous souhaitez également créer une nouvelle connexion, car l'ancienne connexion peut être engagée dans une transaction lancée par un autre appelant. Si vous souhaitez réutiliser des transactions, vous devez 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();
  1. Enfin et surtout, après avoir travaillé avec Jeremy, je suis sûr qu'il dirait que vous devriez avoir des tests unitaires pour toutes ces classes!
33
Chris Shain