Короткий синтаксис 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-а может быть оформлено анонимным методом или лямбда-выражением.