Friday, April 4, 2014

Использование паттерна Repository с BLToolkit. Часть первая

Здравствуйте, уважаемые читатели. В этой статье мы поговорим о таком замечательном компоненте для работы с базами данных, как BLToolkit. Скачать этот продукт вы можете по ссылке. BLToolkit состоит из нескольких модулей, которые позволяют избавиться от рутинных операций с базой данных. Данный тулкит позволяет оптимизировать работу с БД, кешировать запросы, проводить валидацию через атрибуты, поддержка CRUD операций, быстрый мапинг объектов на базу данных и другие возможности. Есть небольшие недочеты с поддержкой LINQ. Пока поддержка LINQ немного хромает, но уже по уровню практически достигла Linq to SQL. Немного проблематичная работа с бинарными данными (Blob поля). 
На некоторых форумах проскальзывает возмущение на тему о том, что в BLToolkit нет поддержки Dependency Injection (DI). Таких разработчиков хотелось бы спросить, зачем лепить в данный набор компонентов еще и DI. Подружить BLToolkit с каким-то популярным бесплатным IoC контейнером не составляет проблем. Например, для меня не представляло сложностей подружить данный тулкит с Ioc контейнером Autofac. Некое неудобство составляет отсутствие версионности. Удобно, что данный набор компонентов можно скачать через Package Manager Console (NuGet).
Цель данной статьи заключается не в том, чтобы показать, как просто работать с BLToolkit, для этого есть множество примеров на официальном сайте bltoolkit.net, а в том, чтобы продемонстрировать, как можно работать с BLToolkit с использованием паттерна Repository с Domain Driven Design (DDD). Пример паттерна Repository рассматривается в книге Эрика Эванса "Предметно-ориентированное проектирование(DDD). Структуризация сложных программных систем"
Сама область разработки достаточно сложна и подразумевает наличие знаний у разработчика по проектированию программного обеспечения (ПО). Мы попробуем немного приоткрыть в этой статье этот вариант использования данного паттерна для реальных примеров. Также одна из особенностей данной статьи заключается в том, что для примера мы создадим GenericRepository. Многие критикуют использования generic репозитория. Пример, почему нельзя использовать данный паттерн, можно посмотреть в статье "If you're going to use repositories, don't have genericor base repositories...".  Не буду вдаваться в полемику с автором приведенной выше статьи и соглашусь с его выводами. Но на каждое действие есть противодействие. Например, классический паттерн Singleton тоже считается антипаттерном, но он до сих пор используется в .NET Framework и позволяет решить множество проблем. Одна из проблем данного паттерна заключается в проблеме с тестированием. Это надуманная проблема. Если проблематично какую-то логику протестировать через Mock, то можно это сделать через фейки и стабы. Подробнее об этом можно посмотреть в блоге Сергея Теплякова. Поэтому мы рассмотрим, как GenericRepository позволяет решить 90% проблем с базой данных, а об остальных 10%, которых нужно запретить использовать, например, операцию delete, будет написан другой интерфейс. Если наше решение удовлетворяет поставленную задачу, почему бы его не использовать? 
Отойдем от теории и приступим к практике. Для начала создадим тестовое консольное приложение UsingGenericRepository, в котором будем тестировать нашу работу с репозиторием.
Затем нужно создать заготовку для модели данных и для самого репозитория. Для моделей мы добавим в наше приложение новый проект Class Library, который назовем UsingGenericRepository.Model. Задача данной библиотеки будет заключаться в мапинге классов на базу данных как через явный мапинг, так и через POCO модель.
Для репозитория добавим файл проекта Class Library и назовем его UsingGenericRepository.Repository.
Созданные классы Class1 в данных библиотеках можно удалить. После создания ваша архитектура будет выглядеть следующим образом:
Далее для этих проектов необходимо через NuGet поставить BLToolkit. Как это сделать, можно посмотреть на первом рисунке в начале статьи либо вызвать Package Manager Console, как показано на рисунке ниже.
Устанавливаем необходимый компонент, вызвав строку Install-Package BLToolkit.
Сначала перейдем в наш проект UsingGenericRepository.Model и добавим базовою реализацию для классов, которые будут представлять модель классов нашей базы данных. Для этого создадим базовый интерфейс IEntity, как показано ниже на рисунке.
public interface IEntity
{
       long Id { get; set; }
       bool IsDirty { get; }
       bool IsNew { get; }

       Dictionary<string, object> Parameters { get; }

       IEntity CopyTo(IEntity entity);
       object Clone();
       object GetValue(string fieldName);
       void SetValue(string fieldName, object value);
}
Неудобства данной реализации заключаются в том, что для данного примера в каждой таблице должно быть поле Id, которое является автоинкрементным полем, а также первичным ключом. Это проблема для более сложных данных, но и ее можно решить, например, построив такой базовый репозиторий для тех таблиц, у которых есть первичный ключ Id, а для остальных сделать реализацию через EditableObject<T> или EditableObject, или, как вариант, через DataAccessor, как в примере с официальной документации. В следующей статье, написанной с использованием паттерна репозиторий под Entity Framework 6, я решил эту проблему по-другому. Если вам будет интересно, вы сможете посмотреть альтернативный подход. В любом случае, эту проблему с Id полем можно решить. Далее создадим базовый контейнер EntityBase, который будет использоваться для хранения сущности с базы данных.
/// <summary>
/// Базовый контейнер для передачи в БД информации про сущность
/// </summary>
[Serializable]
public abstract class EntityBase : EditableObject, IEntity
{
       /// <summary>
       /// Свойство определяющее,обьект новый или нет
       /// </summary>
       [MapIgnore]
       public bool IsNew
       {
             get
             {
                    return Id <= 0;
             }
       }

       #region IEntity Members

       /// <summary>
       /// Идентификатор обьекта
       /// </summary>
       [PrimaryKey, NonUpdatable]
       public abstract long Id { get; set; }

       /// <summary>
       /// Параметры обьекта
       /// </summary>
       [MapIgnore]
       public virtual Dictionary<string, object> Parameters
       {
             get
             {
                    var objMapper = Map.GetObjectMapper(GetType());

                    var result = (from fieldName in objMapper.FieldNames
                                               select new
                                               {
                                                      FieldName = fieldName,
                                                      Value = objMapper.GetValue(this, fieldName)
                                               })
                                               .ToDictionary(o => o.FieldName, o => o.Value);

                    return result;
             }
       }

       /// <summary>
       /// Извлечение значения из обьекта, по имени поля в бд
       /// </summary>
       /// <param name="fieldName"></param>
       /// <returns></returns>
       public object GetValue(string fieldName)
       {
             if (string.IsNullOrEmpty(fieldName))
                    throw new ArgumentNullException();

             var objMapper = Map.GetObjectMapper(GetType());
             return objMapper.GetValue(this, fieldName);
       }

       /// <summary>
       /// Установка значения по имени поля в бд
       /// </summary>
       /// <param name="fieldName"></param>
       /// <param name="value"></param>
       public void SetValue(string fieldName, object value)
       {
             if (string.IsNullOrEmpty(fieldName))
                    throw new ArgumentNullException();
             var objMapper = Map.GetObjectMapper(GetType());
             objMapper.SetValue(this, fieldName, value);
       }

       /// <summary>
       /// Копирование значений одного обьекта в другой
       /// </summary>
       /// <param name="entity">обьект в который необходимо скопировать</param>
       /// <returns></returns>
       public IEntity CopyTo(IEntity entity)
       {
             return CopyTo((EntityBase)entity);
       }

       /// <summary>
       /// Клонирование обьекта
       /// </summary>
       /// <returns></returns>
       object IEntity.Clone()
       {
             return Clone();
       }

       #endregion

       /// <summary>
       ///  Копирование значений одного объекта в другой
       /// </summary>
       /// <param name="obj">обьект в который необходимо скопировать</param>
       /// <returns></returns>
       public virtual EntityBase CopyTo(EntityBase obj)
       {
             var properties = GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

             foreach (var info in properties)
             {
                    if (!info.CanRead)
                           continue;

                    var propDest = obj.GetType().GetProperty(info.Name);
                    if (propDest == null || !propDest.CanWrite)
                           continue;

                    if (info.PropertyType != propDest.PropertyType)
                           continue;

                    var value = info.GetValue(this, null);
                    propDest.SetValue(obj, value, null);
             }

             return obj;
       }

       /// <summary>
       /// Клонирование обьекта
       /// </summary>
       /// <returns></returns>
       public virtual EntityBase Clone()
       {
             var formatter = new BinaryFormatter();

             using (var stream = new MemoryStream())
             {
                    formatter.Serialize(stream, this);
                    //
                    // Перемещаемся на начало, чтобы выполнить десериализацию
                    //
                    stream.Position = 0;
                    return formatter.Deserialize(stream) as EntityBase;
             }
       }

       public static T Copy<T>(T entity)
             where T : EntityBase
       {
             return TypeAccessor<T>.Copy(entity, TypeAccessor<T>.CreateInstance());
       }
}

В BLToolkit уже имеется реализация классов EntityBase и EntityBase<T>, которые наследуются соответственно от классов EditableObject<T> или EditableObject, но мы создали новый класс для того чтобы разрулить ситуацию с объектом, который используется на данный момент, является он новым или он уже сохранен в БД. Этот подход мне не очень нравится, но если грамотно построить работу с данным объектом, то данный подход имеет право на жизнь. Если вам не нужны такие возможности, а в 90% так и будет, вам можно не заморачиваться с такими сложностями, а сделать, как описано выше, способами решения через EditableObject. Продолжение на странице второй. 


No comments:

Post a Comment