Архитектура доступа к БД в коде приложения - PullRequest
2 голосов
/ 10 марта 2011

Я работал над большим проектом, в котором довольно интенсивно используется реляционная БД.Проект находится на C # и не использует ORM.Мне трудно работать с приложением из-за того, как оно обращается к БД в коде приложения, но у меня недостаточно опыта работы с большими проектами, чтобы сказать, как оно может быть лучше (не то, чтобы я думал, что это хорошая идеяизменить огромное количество устаревшего кода, но я хочу знать, как сделать это лучше для следующего проекта).Мне все равно, если ваш ответ имеет какое-либо отношение к C # или использованию ORM или нет, я просто хочу прочитать различные подходы, принятые для решения этой проблемы.

Вот краткое описание того, как работает этот проект:

  • Существует довольно много таблиц, представлений и небольшого количества хранимых процедур.
  • В коде приложения есть уровень доступа к данным, который обрабатывает необработанный доступ к БД (этот слой представляет собой набор функций, которые выглядели бы как GetUserById(id), GetUserByLastName(lastName), AddUser(firstname, lastName), GetCommentsByDateAndPostId(date, postId), GetCommentsByDateAndPostIdSortedByDate(date, postId) и т. д.).Все эти функции вызывают рукописный SQL-запрос и возвращают представление таблицы в памяти (т. Е. results[0].lastName - это столбец lastName строки 0).В C # это DataTable.
  • Существует уровень над уровнем доступа к данным для правил бизнес-обработки.Он является оберткой для каждой из функций доступа к данным, но может выполнить пару проверок бизнес-логики, прежде чем вызывать соответствующую (то есть одноименную) функцию доступа к данным.Он возвращает то же самое, что и уровень доступа к данным во всех случаях.Код приложения ТОЛЬКО получает доступ к БД через этот уровень, а не непосредственно к уровню доступа к данным.
  • Нет одноразовых запросов в дикой природе.Существует взаимно-однозначное соответствие между запросами и функциями на уровне доступа к данным (и, следовательно, на уровне бизнес-логики).Поскольку база данных нормализована, для большинства запросов существует представление, поскольку объединения необходимы.
  • Здесь и там очень редко используется хранимая процедура

Итак, если я хочу сегодня получить доступ к БД по-новому, мне нужно изменить БД, а затем создатьфункция уровня доступа к данным, которая вызывает пользовательский SQL-запрос к базе данных, а затем создает функцию бизнес-логики, которая вызывает функцию уровня доступа к данным.И затем, возможно, изменить кучу существующих функций, чтобы включить это изменение или нет.Я даже не знаю, с чего начать автоматическое тестирование в такой нестабильной среде.

И это только если я хочу изменить или добавить один столбец БД.Если я хочу добавить новую таблицу, есть куча новых функций, которые нужно добавить для всех способов, которыми она может быть выбрана (комбинации предложений WHERE), или вставлена, или обновлена, или удалена, или отсортирована, и т. Д.

Ответы [ 2 ]

2 голосов
/ 10 марта 2011

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

Например, из вашего описания очевидно, что архитектура четко разделяет функциональные обязанности на уровни.У вас есть презентация (UI), которая взаимодействует с доменом (BLL), который, в свою очередь, использует шаблон репозитория для взаимодействия со своей инфраструктурой (DAL).Кажется, что в вашем BLL уже реализованы сквозные задачи, такие как проверка и безопасность.

Что вы можете сделать, чтобы улучшить этот дизайн, так это включить более сильный Домен путем включения Модели.Отбросьте старые методы ADO.NET DataTable и разработайте строго типизированную модель, отражающую вашу базу данных.Внедрение ORM может очень помочь в этом, поскольку оно может генерировать модель из базы данных и легко вносить изменения.

Я не буду вдаваться в дальнейшие преимущества ORM, как вы пожелаете.Ваш DAL должен вернуть POCOs и Enumerables.Пусть ваш BLL возвращает объекты ответа (мне нравится называть их объектами ответа службы или объектами передачи презентации), которые могут содержать такие вещи, как: данные POCO, результаты обработки ошибок, результаты проверки.

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

public class UserRepository
{
    public User GetUserById(Int32 userId){...}
}

Вы можете создать (используя обобщенные элементы) репозиторий, который реализует IQueryable.Посмотрите на nCommon для хорошего подхода к этому.Это позволит вам сделать что-то вроде:

var userRepository = new EF4Repository<User>(OrmContextFactory.CreateContext(...));
User u = userRepository.Where(user => user.Id == 1).SingleOrDefault();

Плюсы этого в том, что вам нужно только создать бизнес-логику домена.Если вам нужно изменить таблицы базы данных, вам нужно изменить бизнес-логику только один раз.Однако этот запрос теперь существует в бизнес-логике и просто использует «Репозиторий» в качестве среды для связи с вашей базой данных, что некоторые считают неправильным.


ОБНОВЛЕНИЕ

Вы можете использовать обобщения для создания простого объекта ответа.Пример:

[DataContract(Name = "ServiceResponseOf{0}")]
public class ServiceResponse<TDto> : ResponseTransferObjectBase<TDto> where TDto : IDto
{
    #region Constructors

    /// <summary>
    /// Initializes a new instance of the <see cref="ServiceResponse&lt;TDto&gt;"/> class.
    /// </summary>
    /// <param name="error">The error.</param>
    /// <remarks></remarks>
    public ServiceResponse(ServiceErrorBase error)
        : this(ResponseStatus.Failure, null, new List<ServiceErrorBase> {error}, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ServiceResponse&lt;TDto&gt;"/> class.
    /// </summary>
    /// <param name="errors">The errors.</param>
    /// <remarks></remarks>
    public ServiceResponse(IEnumerable<ServiceErrorBase> errors)
        : this(ResponseStatus.Failure, null, errors, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ServiceResponse&lt;TDto&gt;"/> class with a status of <see cref="ResponseStatus.Failure"/>.
    /// </summary>
    /// <param name="validationResults">The validation results.</param>
    public ServiceResponse(MSValidation.ValidationResults validationResults)
        : this(ResponseStatus.Failure, null, null, validationResults)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ServiceResponse&lt;TDto&gt;"/> class with a status of <see cref="ResponseStatus.Success"/>.
    /// </summary>
    /// <param name="data">The response data.</param>
    public ServiceResponse(TDto data)
        : this(ResponseStatus.Success, new List<TDto> { data }, null, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ServiceResponse&lt;TDto&gt;"/> class with a status of <see cref="ResponseStatus.Success"/>.
    /// </summary>
    /// <param name="data">The response data.</param>
    public ServiceResponse(IEnumerable<TDto> data)
        : this(ResponseStatus.Success, data, null, null)
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="ServiceResponse&lt;TDto&gt;"/> class.
    /// </summary>
    /// <param name="responseStatus">The response status.</param>
    /// <param name="data">The data.</param>
    /// <param name="errors">The errors.</param>
    /// <param name="validationResults">The validation results.</param>
    /// <remarks></remarks>
    private ServiceResponse(ResponseStatus responseStatus, IEnumerable<TDto> data, IEnumerable<ServiceErrorBase> errors, MSValidation.ValidationResults validationResults)
    {
        Status = responseStatus;
        Data = (data != null) ? new List<TDto>(data) : new List<TDto>();

        Errors = Mapper.Map<IEnumerable<ServiceErrorBase>, List<ServiceError>>(errors) ??
                 new List<ServiceError>();

        ValidationResults = 
            Mapper.Map<MSValidation.ValidationResults, List<IValidationResult>>(validationResults) ??
            new List<IValidationResult>();
    }

    #endregion

    #region Properties

    /// <summary>
    /// Gets the <see cref="IDto"/> data.
    /// </summary>
    [DataMember(Order = 0)]
    public List<TDto> Data { get; private set; }

    [DataMember(Order = 1)]
    public List<ServiceError> Errors { get; private set; }

    /// <summary>
    /// Gets the <see cref="ValidationResults"/> validation results.
    /// </summary>
    [DataMember(Order = 2)]
    public List<IValidationResult> ValidationResults { get; private set; }

    /// <summary>
    /// Gets the <see cref="ResponseStatus"/> indicating whether the request failed or succeeded.
    /// </summary>
    [DataMember(Order = 3)]
    public ResponseStatus Status { get; private set; }

    #endregion
}

Этот класс является базовым объектом ответа, который я использую для возврата результатов из моего домена на мой уровень обслуживания или в мою презентацию.Он может быть сериализован и поддерживает блок проверки MS Enterprise Library.Для поддержки проверки он использует AutoMapper для преобразования результатов проверки Microsoft в мой собственный объект ValidationResult.Я не рекомендую пытаться сериализовать классы MS, так как они оказались подвержены ошибкам при использовании в сервисах.

Перегруженные конструкторы позволяют вам предоставлять одно poco или множество pocos.POCOs против DataTables ... каждый раз, когда вы можете использовать строго типизированные объекты, это всегда лучше.С T4 шаблонами ваш POCO может автоматически генерироваться из модели ORM.POCO также могут быть легко сопоставлены с DTO для сервисных операций и наоборот.Также больше нет необходимости в DataTables.Вместо List вы можете использовать BindingList для поддержки CRUD с привязкой данных.

Возврат POCO без заполнения всех его свойств - это прекрасно.В Entity Framework это называется проекцией.Обычно я создаю собственные DTO для этого вместо сущностей моего домена.


UPDATE

Пример класса ValidationResult:

/// <summary>
/// Represents results returned from Microsoft Enterprise Library Validation. See <see cref="MSValidation.ValidationResult"/>.
/// </summary>
[DataContract]
public sealed class ValidationResult : IValidationResult
{
    [DataMember(Order = 0)]
    public String Key { get; private set; }

    [DataMember(Order = 1)]
    public String Message { get; private set; }

    [DataMember(Order = 3)]
    public List<IValidationResult> NestedValidationResults { get; private set; }

    [DataMember(Order = 2)]
    public Type TargetType { get; private set; }

    public ValidationResult(String key, String message, Type targetType, List<ValidationResult> nestedValidationResults)
    {
        Key = key;
        Message = message;
        NestedValidationResults = new List<IValidationResult>(nestedValidationResults);
        TargetType = targetType;
    }
}

Пример кода AutoMapper для перевода MicrosoftРезультаты проверки для результата проверки ValidationResult:

Mapper.CreateMap<MSValidation.ValidationResult, IValidationResult>().ConstructUsing(
            dest =>
            new ValidationResult(
                dest.Key,
                dest.Message,
                dest.Target.GetType(),
                dest.NestedValidationResults.Select(mappingManager.Map<MSValidation.ValidationResult, ValidationResult>).ToList()));
0 голосов
/ 10 марта 2011

Я бы рекомендовал использовать шаблон Facade для инкапсуляции всех вызовов доступа к данным в одном объекте. Затем преобразуйте каждый существующий вызов доступа к данным в вызов фасадного объекта.

Я написал более подробное объяснение реализации шаблона фасада в ответ на другой вопрос на Лучший подход к архитектуре интеграции двух отдельных баз данных? .

...