Короткий синтаксис ADO .NET (часть II)
Короткий синтаксис ADO .NET (часть IV)
Приступим к созданию соединений и команд. Я для этого предпочитаю использовать некий класс DbDriver, параметризованный ConnectionString-ом и соответствующим DbProviderFactory классом. DbDriver создает соединения сразу же инициированные ConnectionString-ом, и команды, инициированные соединением. Следующий интерфейс как раз описывает эту сущность:
public interface IDbDriver
{
IDbConnection MakeConnection();
IDbCommand MakeCommand(IDbConnection connection, string commandText);
IDbCommand MakeCommand(string text);
int ExecuteNonQuery(string commandText);
int ExecuteNonQuery(IDbConnection connection, string commandText);
void ExecuteReader(Action<IDataReader> action, string commandText);
void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText);
}Вообще, рекомендуется в перегружаемых методах опциональный параметр писать в конце списка параметров. Забегая вперед, скажу, что это не окончательная сигнатура методов создания и выполнения команд. Перегруженные методы MakeCommand нужны для двух вариантов создания команд. Один вариант (принимающий IDbConnection) нужен для создания команд в рамках указанного соединения, когда потребуется выполнение более одной команды, другой вариант (без соединения) - когда нам требуется выполнить лишь одну команду в соединении.Методы выполнения команд (ExecuteNonQuery и ExecuteReader) перегружены со следующей целью. Один вариант принимает соединение, создает и инициирует команду, открывает открывает соединение (если не было открыто), выполняет команду, закрывает (если было закрыто), уничтожает команду и возвращает результат. Второй вариант - создает соединение, вызывает первый вариант, после чего уничтожает соединение.
public class DbDriver : IDbDriver
{
public DbDriver(string connectionString, DbProviderFactory providerFactory)
{
if (providerFactory == null)
{
throw new ArgumentNullException("providerFactory");
}
ProviderFactory = providerFactory;
ConnectionString = connectionString;
}
public string ConnectionString { get; private set; }
public DbProviderFactory ProviderFactory { get; private set; }
public IDbConnection MakeConnection()
{
IDbConnection result = ProviderFactory.CreateConnection();
result.ConnectionString = ConnectionString;
return result;
}
public IDbCommand MakeCommand(IDbConnection connection, string commandText)
{
if (connection == null)
{
throw new ArgumentNullException("connection");
}
var result = connection.CreateCommand();
result.CommandText = commandText;
return result;
}
public IDbCommand MakeCommand(string commandText)
{
return MakeCommand(MakeConnection(), commandText);
}
....
}Выше приведена реализация конструктора класса и производящих методов. Надеюсь, что комментарии излишни. Далее реализация методов, выполняющих команды: public int ExecuteNonQuery(string commandText)
{
using (var connection = MakeConnection())
{
return ExecuteNonQuery(connection, commandText);
}
}
public int ExecuteNonQuery(IDbConnection connection, string commandText)
{
if (connection == null)
{
throw new ArgumentNullException("connection");
}
using (var command = MakeCommand(connection, commandText))
using (connection.CreateSession())
{
return command.ExecuteNonQuery();
}
}
public void ExecuteReader(IDbConnection connection, Action<IDataReader> action, string commandText)
{
if (connection == null)
{
throw new ArgumentNullException("connection");
}
using (var command = MakeCommand(connection, commandText))
using (connection.CreateSession())
using (var reader = command.ExecuteReader())
{
action(reader);
}
}
public void ExecuteReader(Action<IDataReader> action, string commandText)
{
using (var connection = MakeConnection())
{
ExecuteReader(connection, action, commandText);
}
}Методы Execute*** делают попытку открыть соединение в самый последний момент перед обращением к команде и закрывают соединение, если оно было закрыто перед вызовом, сразу после выполнения команды. Повторюсь, гибкость метода CreateSession позволяет одними методами работать как с первоначально закрытым соединением с БД, так и с уже октрытым. ExecuteReader принимает Action<IDataReader>. Это связано с тем, что IDataReader должен быть освобожден до закрытия соединения. Если метод ExecuteReader будет закрывать соединение, т.е. если соединение было закрыто перед вызовом, то к тому моменту требуется прочитать все что надо из IDataReader-а. Метод ExecuteScalar я даже не стал включать в пример, т.к. он работает полностью аналогично методу ExecuteReader.Unit тесты приводить не буду. Но один интеграционный, пожалуй приведу. Он довольно показателен по части того, что достигнуто:
[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(
reader => table.Load(reader),
"SELECT OrderId FROM [Order Details]");
Assert.IsTrue(table.Rows.Count > 0);
}Код, что отвечает непосредственно за обращение к БД занимает всего 3 строчки. В рамках обращения к одному методу выполняются действия, перечисленные в конце первой части этой серии постов.Почти все! Не хватает только чего-то для работы с параметрами команд. А это в следующем посте...
Плохо, что Action и select который выполняет reader в общем случае могут находится ооочень далеко.
ОтветитьУдалитьПоправив немного селект, придется далеко бегать за action'ом и исправлять его.
да, согласен, могут. При работе с базой данных из кода все может быть очень далеко, особенно если select в хранимой процедуре.
ОтветитьУдалитьТем не менее, предложенный мной подход не обязывает разносить select и чтение из reader-а дальше друг от друга, чем это разработчик привык делать )). А при желании чтение из reader-а может быть оформлено анонимным методом или лямбда-выражением.