пятница, 15 августа 2008 г.

Короткий синтаксис ADO .NET (часть IV)

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис ADO .NET (часть II)
Короткий синтаксис ADO .NET (часть III)

В предыдущих частях было описано все что необходимо для обращения к БД за один вызов, кроме разве что передачи параметров. В ADO .NET уже проделана вся работа по унифицированному способу задания параметров. Осталось лишь воспользоваться им.

Задумка в том, чтобы объявлять параметры к команде в том же обращении (ExecuteReader, ExecuteScalar либо ExecuteNonQuery) через запятые. Для каждого параметра достаточно передать только 2 значения - имя параметра и значение параметра. А тип параметра будет определяться типом значения. Определим следующий класс:
public class Parameter
{
    public string ParameterName { get; private set; }

    public DbType DbType { get; private set; }

    public object Value { get; private set; }

    private Parameter(string name, DbType type, object value)
    {
        ParameterName = name;
        DbType = type;
        Value = value;
    }

    public void ApplyParameter(IDbCommand command)
    {
        IDbDataParameter parameter = command.CreateParameter();
        parameter.ParameterName = ParameterName;
        parameter.DbType = DbType;

        if (ReferenceEquals(Value, null))
        {
            parameter.Value = DBNull.Value;
        }
        else
        {
            parameter.Value = Value;
        }

        command.Parameters.Add(parameter);
    }

    ...
}
Метод ApplyParameter создает унифицированным способом экземпляр параметра, передает ему хранящиеся в экземпляре имя, унифицированный тип и значение параметра, добавляет созданный параметр в коллекцию параметров команды. Обратите внимание, что конструктор класса Parameter объявлен с private модификатором видимости. Это потому, что я параноидально опасаюсь создания экземпляров с типом параметра не согласованным со значением параметра. Определение типов параметра возьмет на себя серия производящих методов.

Метод ConvertToObject<T>(T? value) конвертирует переданные nullable значения в object тип. В сочетании с методом ApplyParameter они удачно проецируют nullable типы на значения параметров.
internal static object ConvertToObject<T>(T? value)
    where T : struct
{
    if (value.HasValue)
    {
        return value.Value;
    }
    else
    {
        return null;
    }
}

public static Parameter Make(string name, string value)
{
    return new Parameter(name, DbType.String, value);
}

public static Parameter Make(string name, Guid? value)
{
    return new Parameter(name, DbType.Guid, ConvertToObject(value));
}

public static Parameter Make(string name, int? value)
{
    return new Parameter(name, DbType.Int32, ConvertToObject(value));
}

public static Parameter Make(string name, bool? value)
{
    return new Parameter(name, DbType.Boolean, ConvertToObject(value));
}

public static Parameter Make(string name, double? value)
{
    return new Parameter(name, DbType.Double, ConvertToObject(value));
}

public static Parameter Make(string name, DateTime? value)
{
    return new Parameter(name, DbType.DateTime, ConvertToObject(value));
}

public static Parameter Make(string name, decimal? value)
{
    return new Parameter(name, DbType.Decimal, ConvertToObject(value));
}
Таким образом, обращение
Make("@Value", (int?)null)
сформирует параметр целого типа со значением DbNull.Value. Почему производящие методы, а не перегруженные конструкторы? Просто в некоторых случаях могут потребоваться делегаты (например Func<string, int?, Parameter>), а делегат можно создать только для метода. Для добавления списка параметров к методам Execute*** воспользуемся ключевым словом params.

Вот обновленный интерфейс IDbDriver:
public interface IDbDriver
{
    IDbConnection MakeConnection();

    IDbCommand MakeCommand(IDbConnection connection, string commandText, params Parameter[] parameters);
    IDbCommand MakeCommand(string text, params Parameter[] parameters);

    int ExecuteNonQuery(string commandText, params Parameter[] parameters);
    int ExecuteNonQuery(IDbConnection connection, string commandText, params Parameter[] parameters);

    void ExecuteReader(Action<IDataReader> action, string commandText, params Parameter[] parameters);
    void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText, params Parameter[] parameters);
}
Не составит сложности модифицировать методы Execute*** и протянуть параметр parameters до метода MakeCommand. Вот его новая реализация:
public IDbCommand MakeCommand(IDbConnection connection, string commandText, params Parameter[] parameters)
{
    if (connection == null)
    {
        throw new ArgumentNullException("connection");
    }
    var result = connection.CreateCommand();

    result.CommandText = commandText;
    foreach (Parameter parameter in parameters)
    {
        parameter.ApplyParameter(result);
    }
    return result;
}
Осталось привести пример обращения к БД для выполнения команды с параметром:
[TestMethod]
public void ExecuteReaderRealTest()
{
    var dbDriver = new DbDriver(
        "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True",
        SqlClientFactory.Instance);

    DataTable table = new DataTable();
    dbDriver.ExecuteReader(
         table.Load,
         "SELECT * FROM [Order Details] WHERE UnitPrice > @Price",
         Parameter.Make("@Price", 20));

    Assert.IsTrue(table.Rows.Count > 0);
}
И в заключение содержательной части пример с выполнением более чем одной команды в рамках одного соединения:
static void LoadTable(DataTable table, IDbCommand command)
{
    using (var reader = command.ExecuteReader())
    {
        table.Load(reader);
    }
}

[TestMethod]
public void MultiCommandTest()
{
    var dbDriver = new DbDriver(
        "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=True",
        SqlClientFactory.Instance);

    var dataSet = new DataSet();
    var orderDetails = dataSet.Tables.Add("Order Details");
    var orders = dataSet.Tables.Add("Orders");

    int orderId = 10248;

    using (var connection = dbDriver.MakeConnection())
    {
        var orderIdParameter = Parameter.Make("@OrderId", orderId);
      
        Func<string, IDbCommand> makeCommand =
            x => dbDriver.MakeCommand(connection, x, orderIdParameter);
                    
        using (var selectOrders = makeCommand("SELECT * FROM Orders WHERE OrderId = @OrderId"))
        using (var selectDetails = makeCommand("SELECT * FROM [Order Details] WHERE OrderId = @OrderId"))
        using (connection.CreateSession())
        {
            LoadTable(orders, selectOrders);
            LoadTable(orderDetails, selectDetails);
        }
    }

    Assert.IsTrue(orderDetails.Rows.Count > 0);
}
Обратите внимание, для короткой записи создания команды я использовал анонимный метод, который использует один экземпляр класса Parameter для формирования всех команд. При чтении DataSet наборов данных довольно часто используется одинаковый набор параметров при обращении к БД за разными таблицами. На этом и сыграл. Запись получилась компактной и наглядной. В первом посте я поведал об истории моего собеседования.

Надеюсь, что теперь вы меня поймете, что используя такую обвязку к ADO .NET не мудрено забыть кое-что из последовательности действий, описываемых в MSDN... Применяя такую обвязку к ADO, будьте внимательны на собеседованиях ;)

Содержательная часть серии постов об короткой нотации ADO .NET закончилась. Осталось только поудивляться тому, что разработчики Microsoft все необходимое сделали сами, как то инвариантные относительно движка БД способы создания соединений, команд, параметров команд, и даже предусмотрели инвариантные типы параметров. Однако, по каким-то причинам Microsoft просто не довела эту кухню до этого логического конца. Почему-то их логическим концом стал безумный кодогенератор дизайнера адаптеров данных, который генерирует дикое количество нечитаемого кода, завязанного на тип БД. Если разработчик работает с SqlServer, то дизайнер генерирует соединения SqlConnection, команды SqlCommand, параметры SqlParameter и совершенно не ясно, как модифицировать этот код для работы с другим движком БД, либо с несколькими. К тому же, вся документация ADO .NET кишит примерами, завязанными на конкретные движки БД. Скромно замечу, что преподнесенный мной код обвязки абсолютно инвариантен к типу БД. Но заслуга в этом разработчиков Microsoft.

12 комментариев:

  1. Да, интересная обёрточка, попробую применить, когда в следующий раз буду бороться с ADO.NET. Вот только пока проектов под третий шарп пока нету.

    ОтветитьУдалить
  2. serge>Вот только пока проектов под третий шарп пока нету.

    Большая часть кода обертки может быть адаптирована под C# 2.0. Кажется единственное, что не получится адаптировать - extension метод CreateSession(this IDbConnection). А без него можно легко обойтись.

    ОтветитьУдалить
  3. На правах бреда выскажу гипотезу о том, что у MS есть цель привязать как можно больше народа к платформе Windows. Эту цель они достигают в два этапа: Во-первых, привязывают разработчиков к .NET, а во-вторых, привязывают .NET к Visual Studio, которая уже привязана к Windows. Так вод для привязки к VS приходится пропагандировать кодогенераторы.

    Есть конечно и более реалистичные гипотезы. Например, гипотеза "умных людей везде не хватает." :)

    Кстати, мы у себя такой же велосипед изобрели. Только назвали его OpenedConnection. :) Может не столь красивый, но смысл тот же - обернуть в человеческий вид ADO.NET.

    ОтветитьУдалить
  4. xoposhiy>На правах бреда выскажу гипотезу о том, что у MS есть цель привязать как можно больше народа к платформе Windows. Эту цель они достигают в два этапа

    Не такой уж бред. Это действительно так, и это нормально (Sun имеет аналогичную цель). Только этапов в достижении этой цели куда больше, и кодогенераторы играют там весьма небольшую роль.

    Мне кажется, что такие велосипеды, которые изобретаются в любой компании, знакомой со словами "рефакторинг" и "дублирование кода", для MS лишь полумеры. Наверняка такие же велосипеды изобретены и в MS, просто продвижение их в массы дорого и не нужно (есть LINQ2SQL, который еще больше привяжет к платформе). Т.е. было "не до того".

    ОтветитьУдалить
  5. А вы не могли бы выложить сиходники этой библиотеки?

    ОтветитьУдалить
  6. >А вы не могли бы выложить сиходники этой библиотеки?

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

    ОтветитьУдалить
  7. Спасибо, но в таком виде уже скопипастил :)))

    Не хочется тащить на гуевый проект со встроенной бд (sqlite) hibernate, а ваше творчество более-менее подходит...

    ОтветитьУдалить
  8. Незачто. Рад что кому-то пригодилось.

    Вот до кучи идея передачи параметров в виде экземпляра анонимного типа (как выяснилось, не моя идея):

    public IDbCommand MakeCommand(string commandText, T parameters)
    {
    return MakeCommand(commandText, Parameter.Convert2Parameters(parameters));
    }
    // usage
    MakeCommand("SELECT ...", new { age = 20 });

    ОтветитьУдалить
  9. Я правильно понимаю, из этого new { age = 20 } с помощью рефлекшн извлекать имена свойств и их типы и формировать список параметров? Прикольно однако

    ОтветитьУдалить
  10. Да, правильно. Но есть еще способы: Emit и деревья выражений.
    кроме анонимных типов можно обрабатывать и нормальные.

    ОтветитьУдалить
  11. Еще не ясно как реализована
    IDbCommand MakeCommand(string text, params Parameter[] parameters);

    без connection. Если не сложно киньте таки исходники на adm-Beat@yandex.ru

    ОтветитьУдалить
  12. на сколько я помню, MakeCommand создает команду с помощью метода connection-а. Мистики там точно нет.

    ОтветитьУдалить