Короткий синтаксис 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. Вот только пока проектов под третий шарп пока нету.
ОтветитьУдалитьserge>Вот только пока проектов под третий шарп пока нету.
ОтветитьУдалитьБольшая часть кода обертки может быть адаптирована под C# 2.0. Кажется единственное, что не получится адаптировать - extension метод CreateSession(this IDbConnection). А без него можно легко обойтись.
На правах бреда выскажу гипотезу о том, что у MS есть цель привязать как можно больше народа к платформе Windows. Эту цель они достигают в два этапа: Во-первых, привязывают разработчиков к .NET, а во-вторых, привязывают .NET к Visual Studio, которая уже привязана к Windows. Так вод для привязки к VS приходится пропагандировать кодогенераторы.
ОтветитьУдалитьЕсть конечно и более реалистичные гипотезы. Например, гипотеза "умных людей везде не хватает." :)
Кстати, мы у себя такой же велосипед изобрели. Только назвали его OpenedConnection. :) Может не столь красивый, но смысл тот же - обернуть в человеческий вид ADO.NET.
xoposhiy>На правах бреда выскажу гипотезу о том, что у MS есть цель привязать как можно больше народа к платформе Windows. Эту цель они достигают в два этапа
ОтветитьУдалитьНе такой уж бред. Это действительно так, и это нормально (Sun имеет аналогичную цель). Только этапов в достижении этой цели куда больше, и кодогенераторы играют там весьма небольшую роль.
Мне кажется, что такие велосипеды, которые изобретаются в любой компании, знакомой со словами "рефакторинг" и "дублирование кода", для MS лишь полумеры. Наверняка такие же велосипеды изобретены и в MS, просто продвижение их в массы дорого и не нужно (есть LINQ2SQL, который еще больше привяжет к платформе). Т.е. было "не до того".
А вы не могли бы выложить сиходники этой библиотеки?
ОтветитьУдалить>А вы не могли бы выложить сиходники этой библиотеки?
ОтветитьУдалитьСиходники из рабочего проекта выложить не могу. Могу выслать на почту сиходник, который был подготовлен для блога. То есть примерно в том виде, в котором он представлен в блоге, в состоянии далеком от продакш кода.
Спасибо, но в таком виде уже скопипастил :)))
ОтветитьУдалитьНе хочется тащить на гуевый проект со встроенной бд (sqlite) hibernate, а ваше творчество более-менее подходит...
Незачто. Рад что кому-то пригодилось.
ОтветитьУдалитьВот до кучи идея передачи параметров в виде экземпляра анонимного типа (как выяснилось, не моя идея):
public IDbCommand MakeCommand(string commandText, T parameters)
{
return MakeCommand(commandText, Parameter.Convert2Parameters(parameters));
}
// usage
MakeCommand("SELECT ...", new { age = 20 });
Я правильно понимаю, из этого new { age = 20 } с помощью рефлекшн извлекать имена свойств и их типы и формировать список параметров? Прикольно однако
ОтветитьУдалитьДа, правильно. Но есть еще способы: Emit и деревья выражений.
ОтветитьУдалитькроме анонимных типов можно обрабатывать и нормальные.
Еще не ясно как реализована
ОтветитьУдалитьIDbCommand MakeCommand(string text, params Parameter[] parameters);
без connection. Если не сложно киньте таки исходники на adm-Beat@yandex.ru
на сколько я помню, MakeCommand создает команду с помощью метода connection-а. Мистики там точно нет.
ОтветитьУдалить