Как улучшить метод выбора уровня доступа к данным

В последнее время я нахожу, что я пишу методы выбора уровня доступа к данным, где весь код принимает эту общую форму:

public static DataTable GetSomeData( ... arguments)
{
    string sql = " ... sql string here:  often it just a stored procedure name ... ";

    DataTable result = new DataTable();

    // GetOpenConnection() is a private method in the class: 
    // it manages the connection string and returns an open and ready connection
    using (SqlConnection cn = GetOpenConnection())
    using (SqlCommand cmd = new SqlCommand(sql, cn))
    {
        // could be any number of parameters, each with a different type
        cmd.Parameters.Add("@Param1", SqlDbType.VarChar, 50).Value = param1; //argument passed to function

        using (SqlDataReader rdr = cmd.ExecuteReader())
        {
            result.Load(rdr);
        }
    }

    return result;
}

Или вот так:

public static DataRow GetSomeSingleRecord( ... arguments)
{
    string sql = " ... sql string here:  often it just a stored procedure name ... ";

    DataTable dt = new DataTable();

    // GetOpenConnection() is a private method in the class: 
    // it manages the connection string and returns an open and ready connection
    using (SqlConnection cn = GetOpenConnection())
    using (SqlCommand cmd = new SqlCommand(sql, cn))
    {
        // could be any number of parameters, each with a different type
        cmd.Parameters.Add("@Param1", SqlDbType.VarChar, 50).Value = param1; //argument passed to function

        using (SqlDataReader rdr = cmd.ExecuteReader(CommandBehavior.SingleRow))
        {
            dt.Load(rdr);
        }
    }

    if (dt.Rows.Count > 0)
         return dt.Rows[0];
    return null;
}

Эти методы будут вызываться кодом бизнес-уровня, который затем преобразует базовый DataTable или DataRecord в строго типизированные бизнес-объекты, которые может использовать уровень представления.

Так как я использую аналогичный код несколько раз, я хочу убедиться, что этот код лучше всего. Итак, как его можно улучшить? И стоит ли пытаться переместить общий код из этого метода в свой собственный. Если да, то каким будет выглядеть этот метод (в частности, для передачи коллекции SqlParameter)?

Ответы

Ответ 1

Пришлось добавить свои собственные:
Возвращает DataReader из DataLayer в инструкции using

Новый шаблон позволяет мне иметь только одну запись в памяти за раз, но все равно заключает в себя соединение в симпатичном "использовании":

public IEnumerable<T> GetSomeData(string filter, Func<IDataRecord, T> factory)
{
    string sql = "SELECT * FROM [SomeTable] WHERE SomeColumn= @Filter";

    using (SqlConnection cn = new SqlConnection(GetConnectionString()))
    using (SqlCommand cmd = new SqlCommand(sql, cn))
    {
        cmd.Parameters.Add("@Filter", SqlDbType.NVarChar, 255).Value = filter;
        cn.Open();

        using (IDataReader rdr = cmd.ExecuteReader())
        {
            while (rdr.Read())
            {
                yield return factory(rdr);
            }
            rdr.Close();
        }
    }
}

Ответ 2

Один образец, который мне понравился, выглядит так, как клиентский код:

        DataTable data = null;
        using (StoredProcedure proc = new StoredProcedure("MyProcName","[Connection]"))
        {
            proc.AddParameter("@LoginName", loginName);
            data = proc.ExecuteDataTable();
        }

Я обычно делаю подключение необязательным, и я буду кодировать, чтобы вытащить его из раздела конфигурации ConnectStrings или рассматривать его как фактическую строку соединения. Это позволяет мне повторно использовать dal в одном сценарии и частично является привычкой из дней COM +, когда я сохранил строку соединения, используя свойство построения объекта.

Мне это нравится, потому что он легко читается и скрывает от меня весь код ADO.

Ответ 3

Единственное, что я делаю по-другому, - это переключение с моих собственных внутренних вспомогательных методов базы данных на фактический блок приложений доступа к данным http://msdn.microsoft.com/en-us/library/cc309504.aspx

Делает его более стандартизированным/единообразным для других разработчиков, которые знают, что корпоративная библиотека наращивает код.

Ответ 4

Существует так много способов реализовать DBAL, на мой взгляд, вы на правильном пути. Что нужно учитывать при реализации:

  • Вы используете метод factory-like для создания вашего SqlConnection, это незначительная точка, но вы можете сделать то же самое для своей SqlCommand.
  • Длина параметра не обязательна, поэтому вы действительно можете оставить ее вне вызова Parameter.Add.
  • Создайте методы для добавления параметров, пример кода ниже.

Добавьте параметры с помощью DbUtil.AddParameter(cmd, "@Id", SqlDbType.UniqueIdentifier, Id);

internal class DbUtil {

internal static SqlParameter CreateSqlParameter(
    string parameterName,
    SqlDbType dbType,
    ParameterDirection direction,
    object value
) {
    SqlParameter parameter = new SqlParameter(parameterName, dbType);

    if (value == null) {
        value = DBNull.Value;
    }

    parameter.Value = value;

    parameter.Direction = direction;
    return parameter;
}

internal static SqlParameter AddParameter(
    SqlCommand sqlCommand,
    string parameterName,
    SqlDbType dbType
) {
    return AddParameter(sqlCommand, parameterName, dbType, null);
}

internal static SqlParameter AddParameter(
    SqlCommand sqlCommand,
    string parameterName,
    SqlDbType dbType,
    object value
) {
    return AddParameter(sqlCommand, parameterName, dbType, ParameterDirection.Input, value);
}

internal static SqlParameter AddParameter(
    SqlCommand sqlCommand,
    string parameterName,
    SqlDbType dbType,
    ParameterDirection direction,
    object value
) {
    SqlParameter parameter = CreateSqlParameter(parameterName, dbType, direction, value);
    sqlCommand.Parameters.Add(parameter);
    return parameter;
    }
}

Ответ 5

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

Мои мысли о сворачивании вашего собственного кода доступа к данным:

  • С течением времени мне было проще не иметь отдельные объекты DAL/BL, а скорее объединить их в один объект (через некоторое время после достижения этого вывода я узнал его довольно хорошо известный шаблон, а именно ActiveRecord). Это может выглядеть красиво и развязано, чтобы иметь отдельные сборки DAL, но накладные расходы на обслуживание будут складываться. Каждый раз, когда вы добавляете новую функцию, вам нужно будет создать больше кода/изменить другие классы. По моему опыту, команда, которая поддерживает приложение, часто намного меньше, чем первоначальная команда разработчиков, которая ее построила, и они будут ненавидеть дополнительную работу.
  • Для крупных команд может иметь смысл отделить DAL (и пусть группа работает над ней, а остальные), но это создает хороший стимул для раздувания кода.
  • Возвращаясь к вашему конкретному образцу: как вы используете результирующий DataTable? Итерировать строки, создавать типизированные объекты и получать данные из строки? Если да, подумайте о дополнительном DataTable, который вы создали, только для перемещения данных между DAL и BL. Почему бы не взять его непосредственно из DataReader?
  • Также о примере: если вы вернете нетипизированный DataTable, то, я думаю, вы должны использовать имена столбцов (из набора результатов, который возвращает вызов SP) в вызывающем коде. Это означает, что если мне нужно что-то изменить в базе данных, это может повлиять на оба уровня.

Мое предложение (я пробовал оба метода - предложение - это новейший рабочий подход, с которым я столкнулся - со временем он эволюционировал).

  • Создайте базовый класс для типизированных бизнес-объектов.
  • Сохранять состояние объекта в базовом классе (новый, измененный и т.д.)
  • Поместите основные методы доступа к данным в этом классе, как статические методы. С небольшим усилием (подсказка: общие методы + Activator.CreateInstance) вы можете создать один бизнес-объект для каждой строки, возвращенной в считыватель.
  • сделать абстрактный метод в бизнес-объекте для синтаксического анализа данных строки (непосредственно из DataReader!) и заполнить объект.
  • создавать статические методы в производных бизнес-объектах, которые готовят хранимые параметры процесса (в зависимости от различных критериев фильтра) и вызывают общие методы доступа к данным из базового класса.

Цель состоит в том, чтобы закончить использование, например:

List<MyObject> objects = MyObject.FindMyObject(string someParam);

Преимущество для меня состояло в том, что мне нужно только изменить один файл, чтобы справиться с изменениями в именах столбцов базы данных, типах и т.д. (небольшие изменения вообще). С некоторыми хорошо продуманными областями вы можете организовать код так, чтобы они были отдельными "слоями" в одном и том же объекте:). Другое преимущество заключается в том, что базовый класс действительно многократно используется из одного проекта в другой. И разбухание кода минимально (ну, по сравнению с преимуществами. Вы также можете заполнить наборы данных и привязать их к элементам управления пользовательского интерфейса: D

Ограничения - вы получаете один класс для каждого объекта домена (обычно для каждой основной таблицы базы данных). И вы не можете загружать объекты в существующие транзакции (хотя вы могли подумать о передаче транзакции, если у вас есть).

Сообщите мне, если вас интересует более подробная информация. Я мог бы немного расширить ответ.

Ответ 6

Подобно тому, что я разместил здесь

public IEnumerable<S> Get<S>(string query, Action<IDbCommand> parameterizer, 
                             Func<IDataRecord, S> selector)
{
    using (var conn = new T()) //your connection object
    {
        using (var cmd = conn.CreateCommand())
        {
            if (parameterizer != null)
                parameterizer(cmd);
            cmd.CommandText = query;
            cmd.Connection.ConnectionString = _connectionString;
            cmd.Connection.Open();
            using (var r = cmd.ExecuteReader())
                while (r.Read())
                    yield return selector(r);
        }
    }
}

У меня есть эти простые методы расширения, чтобы облегчить вызов:

public static void Parameterize(this IDbCommand command, string name, object value)
{
    var parameter = command.CreateParameter();
    parameter.ParameterName = name;
    parameter.Value = value;
    command.Parameters.Add(parameter);
}

public static T To<T>(this IDataRecord dr, int index, T defaultValue = default(T),
                      Func<object, T> converter = null)
{
    return dr[index].To<T>(defaultValue, converter);
}

static T To<T>(this object obj, T defaultValue, Func<object, T> converter)
{
    if (obj.IsNull())
        return defaultValue;

    return converter == null ? (T)obj : converter(obj);
}

public static bool IsNull<T>(this T obj) where T : class
{
    return (object)obj == null || obj == DBNull.Value;
}

Итак, теперь я могу позвонить:

var query = Get(sql, cmd =>
{
    cmd.Parameterize("saved", 1);
    cmd.Parameterize("name", "abel");
}, r => new User(r.To<int>(0), r.To<string>(1), r.To<DateTime?>(2), r.To<bool>(3)));
foreach (var user in query)
{

}

Это полностью общее, подходит для любой модели, соответствующей интерфейсам ado.net. Объект подключения и считыватель расположены только после перечисления коллекции один раз.

Ответ 7

Простейшее решение:

var dt=new DataTable();
dt.Load(myDataReader);
list<DataRow> dr=dt.AsEnumerable().ToList();