Схема хранилища и карты данных
После многого чтения о репозитории и 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.
Эти параметры отправляются каждым классом репозитория Entities, который унаследован от класса Repository. например:
public class PersonnelRepository : Repository<Personnel>, IPersonnelRepository {
public PersonnelRepository(DataLayer domainContext, PersonnelDataMapper dataMapper)
: base(domainContext, dataMapper) {
}
}
Как вы можете видеть здесь в методе FindOne, я попытался автоматизировать некоторые операции, такие как создание CommandText, тогда я воспользовался своим классом DataLayer для настройки команды и, наконец, выполнил команду для получения IDataReader. Я передаю IDataReader в мой класс DataMapper для сопоставления с Entity.
-
DomainMapper: Наконец, здесь я сопоставляю результат IDataReader с Entities, ниже пример того, как я сопоставляю Personnel entity:
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
Несколько точек:
-
Мне кажется, что в целом у вас хороший дизайн. Частично это подтверждается тем фактом, что вы можете вносить в него изменения, мало влияя на любые классы за пределами тех, которые были изменены (низкое сцепление). Тем не менее, это очень близко к тому, что делает Entity Framework, поэтому, хотя это хороший персональный проект, я бы предпочел использовать EF прежде, чем реализовать его в производственном проекте.
-
Класс DataMapper можно сделать общим (например, GenericDataMapper<T>
) с использованием отражения. Итерации по свойствам типа T с использованием отражения и динамически получать их из строки данных.
-
Предполагая, что вы делаете Generic DataMapper, вы можете подумать о создании метода CreateRepository<T>()
в DataLayer, чтобы пользователям не нужно было беспокоиться о деталях того, какой тип Mapper выбрать.
-
Небольшая критика - вы предполагаете, что все сущности будут иметь один целочисленный идентификатор с именем "Id" и что сохраненные процедуры будут настроены для их получения. Вы можете улучшить свой дизайн здесь, разрешив первичные ключи разных типов, опять же, возможно, используя дженерики.
-
Вероятно, вы не хотите повторно использовать объекты Connection и Command так, как вы. Это не потокобезопасно, и даже если бы это было так, вы столкнулись с некоторыми неожиданными и трудно отлаживающимися условиями гонки вокруг транзакций DB. Вы либо должны создать новые объекты 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();
- И последнее, но не менее важное: работая с Джереми, я уверен, что он скажет, что у вас должны быть модульные тесты для всех этих классов!