Короткий синтаксис 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-а. Мистики там точно нет.
ОтветитьУдалить