Шаблон репозитория и Data Mapper - PullRequest
39 голосов
/ 13 января 2012

После большого прочтения репозитория и Data Mapper я решил реализовать эти шаблоны в тестовом проекте. Поскольку я новичок в этом, я хотел бы узнать ваше мнение о том, как я реализовал их в простом проекте.

Джереми Миллер говорит:

Сделайте какой-нибудь нетривиальный проект персонального кодирования, где вы можете свободно экспериментировать с шаблонами проектирования.

Но я не знаю, правильно ли я все это сделал или нет.

Вот моя структура проекта:

enter image description here

Как вы можете видеть, есть много папок, которые я собираюсь подробно описать ниже.

  • Домен: доменные объекты проекта идут сюда. У меня есть простой класс Personnel, который унаследован от класса EntityBase, класс EntityBase имеет единственное свойство с именем Id.

    public int Id { get; set; }
    
  • Инфраструктура: вот простой уровень доступа к данным с двумя классами. SqlDataLayer - это простой класс, который унаследован от абстрактного класса с именем DataLayer. Здесь я предоставляю некоторые функции, такие как следующий код:

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

добавление параметра в набор параметров команд:

    public override void AddParameter(string key, string value) {
        var parameter = _command.CreateParameter();
        parameter.Value = value;
        parameter.ParameterName = key;

        _command.Parameters.Add(parameter);
    }

выполнение DataReader:

    public override IDataReader ExecuteReader() {
        if (_connection.State == ConnectionState.Closed)
            _connection.Open();

        return _command.ExecuteReader();
    }

и т. Д.

  • Репозиторий: здесь я попытался реализовать шаблон репозитория. IRepository - универсальный интерфейс

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;
    }

Я не выставлял не реализованные методы из IRepository.

Здесь, в классе Generic Repository, я ожидаю, что два параметра в конструкторе: во-первых, это ссылка на мой класс SqlDataLayer, а во-вторых, это ссылка на Entity DataMapper. Эти параметры отправляются каждым классом репозитория сущностей, который унаследован от класса репозитория. например:

public class PersonnelRepository : Repository<Personnel>, IPersonnelRepository {
    public PersonnelRepository(DataLayer domainContext, PersonnelDataMapper dataMapper)
        : base(domainContext, dataMapper) {

    }
}

Как вы можете видеть здесь, в методе FindOne я пытался автоматизировать некоторые операции, такие как создание CommandText, затем я воспользовался своим классом DataLayer, чтобы настроить команду и, наконец, выполнить команду для получения IDataReader. Я передаю IDataReader в свой класс DataMapper для сопоставления с сущностью.

  • DomainMapper: наконец, здесь я сопоставляю результат IDataReader с сущностями, ниже приведен пример того, как я сопоставляю сущность персонала:

    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"])
        };
    }}
    

Использование:

    using (var context = new SQLDataLayer()) {
        _personnelRepository = new PersonnelRepository(context, new PersonnelDataMapper());
            var personnel  = _personnelRepository.FindOne(1);
    }

Я знаю, что допустил много ошибок здесь, поэтому я здесь. Мне нужен ваш совет, чтобы узнать, что я сделал не так или каковы хорошие моменты в этом простом тестовом проекте.

Заранее спасибо.

1 Ответ

33 голосов
/ 15 января 2012

Несколько баллов:

  1. Мне кажется, что в целом у вас хороший дизайн.Отчасти это подтверждается тем фактом, что вы можете вносить в него изменения, оказывая незначительное влияние на любые классы, кроме тех, которые были изменены (слабая связь).Тем не менее, это очень близко к тому, что делает Entity Framework, поэтому, хотя это хороший личный проект, я бы сначала подумал об использовании EF, прежде чем внедрять его в производственный проект.

  2. Ваш класс DataMapperможно сделать общим (скажем, GenericDataMapper<T>), используя отражение. Перебирайте свойства типа T, используя отражение , и динамически извлекайте их из строки данных.

  3. Предполагая, что вы делаете Generic DataMapper, вы можете сделатьCreateRepository<T>() метод DataLayer, так что пользователям не нужно беспокоиться о деталях выбора типа Mapper.

  4. Небольшая критика - вы предполагаете, что все сущности будут иметьодин целочисленный идентификатор с именем «Id», и для него будут созданы хранимые процедуры для их получения.Возможно, вам удастся улучшить свой дизайн здесь, допуская первичные ключи разных типов, опять же, возможно, с помощью обобщенных элементов.

  5. Возможно, вы не хотите повторно использовать объекты Connection и Commandкак ты.Это не потокобезопасно, и даже если бы это было так, вы бы столкнулись с некоторыми неожиданными и трудными для отладки условиями гонки вокруг транзакций с БД.Вы должны либо создавать новые объекты Connection и Command для каждого вызова функции (не забывая об их удалении после завершения работы), либо реализовать некоторую синхронизацию методов, обращающихся к базе данных.

Например, я бы предложил эту альтернативную версию ExecuteReader:

public override IDataReader ExecuteReader(Command command) {
    var connection = new SqlConnection(connString);
    command.Connection = connection;
    return command.ExecuteReader();
}

Ваш старый повторно использовал командный объект, что могло привести к состязанию между многопоточными абонентами.Вы также хотите создать новое соединение, потому что старое соединение может участвовать в транзакции, запущенной другим абонентом.Если вы хотите повторно использовать транзакции, вы должны создать соединение, начать транзакцию и повторно использовать эту транзакцию, пока не выполните все команды, которые вы хотите связать с транзакцией.Например, вы можете создать перегрузку ваших методов ExecuteXXX следующим образом:

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();
И последнее, но не менее важное: поработав с Джереми, я уверен, он скажет, что у вас должны быть модульные тесты для всех этих классов!
...