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

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

Короткий синтаксис ADO .NET (часть I)
Короткий синтаксис 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 строчки. В рамках обращения к одному методу выполняются действия, перечисленные в конце первой части этой серии постов.

Почти все! Не хватает только чего-то для работы с параметрами команд. А это в следующем посте...

2 комментария:

  1. Плохо, что Action и select который выполняет reader в общем случае могут находится ооочень далеко.

    Поправив немного селект, придется далеко бегать за action'ом и исправлять его.

    ОтветитьУдалить
  2. да, согласен, могут. При работе с базой данных из кода все может быть очень далеко, особенно если select в хранимой процедуре.
    Тем не менее, предложенный мной подход не обязывает разносить select и чтение из reader-а дальше друг от друга, чем это разработчик привык делать )). А при желании чтение из reader-а может быть оформлено анонимным методом или лямбда-выражением.

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