Каковы недостатки использования метода, который вызывает делегат для каждой строки в SqlDataReader? - PullRequest
3 голосов
/ 06 февраля 2011

Когда я нахожу новую идею, я всегда придерживаюсь ее и не вижу ее слабых сторон.Плохие вещи случаются, когда я начинаю использовать новую идею в большом проекте, и через несколько месяцев обнаруживаю, что идея была чрезвычайно плохой, и я не должен использовать ее в каком-либо проекте.

Вот почему, имея новуюидея и готовность использовать его в новом крупном проекте, Мне нужно ваше мнение об этом, особенно отрицательное .


В течение долгого времени мне было скучно печатать сноваи снова или скопируйте и вставьте следующие блоки в проекты, где база данных должна быть доступна напрямую:

string connectionString = Settings.RetrieveConnectionString(Database.MainSqlDatabase);
using (SqlConnection sqlConnection = new SqlConnection(connectionString))
{
    sqlConnection.Open();

    using (SqlCommand getProductQuantities = new SqlCommand("select ProductId, AvailableQuantity from Shop.Product where ShopId = @shopId", sqlConnection))
    {
        getProductQuantities.Parameters.AddWithValue("@shopId", this.Shop.Id);
        using (SqlDataReader dataReader = getProductQuantities.ExecuteReader())
        {
            while (dataReader.Read())
            {
                yield return new Tuple<int, int>((int)dataReader["ProductId"], Convert.ToInt32(dataReader["AvailableQuantity"]));
            }
        }
    }
}

Итак, я создал небольшой класс, который позволяет написать что-то подобное, чтобы сделать то же самое, что и выше:

IEnumerable<Tuple<int, int>> quantities = DataAccess<Tuple<int, int>>.ReadManyRows(
    "select ProductId, AvailableQuantity from Shop.Product where ShopId = @shopId",
    new Dictionary<string, object> { { "@shopId", this.Shop.Id } },
    new DataAccess<string>.Yield(
        dataReader =>
        {
            return new Tuple<int, int>(
                (int)dataReader["ProductId"],
                Convert.ToInt32(dataReader["AvailableQuantity"]);
        }));

Второй подход:

  • Короче, чтобы написать,

  • Легче читать (по крайней мере, для меня; некоторые люди могут сказать, что на самом деле это гораздо менее читабельно),

  • Труднее делать ошибки (например, в первом случае я часто забываю открыть соединение перед его использованием, или язабудьте while блок и т. д.),

  • Faster с помощью Intellisense,

  • Гораздо более сжато, особенно для простых запросов.

Пример:

IEnumerable<string> productNames = DataAccess<string>.ReadManyRows(
    "select distinct ProductName from Shop.Product",
    new DataAccess<string>.Yield(dataReader => { return (string)dataReader["ProductName"]; }));

После реализации такой вещи с простыми ExecuteNonQuery, ExecuteScalar и ReadManyRows и универсальным DataAccess<T>.ReadManyRows в небольшом проекте, я был рад видеть, что код намного короче и проще в обслуживании.

Я обнаружил только два недостатка:

  • Некоторые изменения в требованиях потребуют значительных изменений кода.Например, если есть необходимость добавлять транзакции, это будет очень легко сделать с помощью обычного подхода SqlCommand.Если вместо этого используется мой подход, потребуется переписать весь проект для использования SqlCommand s и транзакций.

  • Небольшие изменения на уровне команд потребуют перехода от моего подхода к стандартномуSqlCommand s.Например, при запросе только одной строки необходимо либо расширить класс DataAccess, чтобы включить этот регистр, либо вместо кода следует использовать SqlCommand с ExecuteReader(CommandBehavior.SingleRow).

  • Тамможет быть небольшая потеря производительности (у меня пока нет точных показателей).

Каковы другие слабые стороны этого подхода, особенно для DataAccess<T>.ReadManyRows?

Ответы [ 5 ]

2 голосов
/ 06 февраля 2011

То, что вы пытаетесь достичь, это хорошо, мне действительно нравится такой синтаксис, и я думаю, что он довольно гибкий. Однако я считаю, что вам нужно лучше разрабатывать свои API.

Код читабелен и почти красив, но его трудно понять, в первую очередь из-за большого количества обобщений, которые не имеют особого смысла, если вы не знаете точно, что означает каждый тип. Я бы по возможности использовал вывод общего типа, чтобы устранить некоторые из них. Для этого рассмотрите возможность использования универсальных методов вместо универсальных типов.

Некоторые предложения по синтаксису (у меня сейчас нет компилятора, поэтому они в основном идеи):

Использовать анонимные типы вместо словарей

Тривиально написать помощника, который преобразует анонимный тип в словарь, но я думаю, что это значительно улучшает запись, и вам не нужно писать new Dictionary<string, object>.

Использовать Tuple.Create

Этот статический метод был создан, чтобы избежать явного указания типов.

Создание обёртки со строгим шрифтом для DataReader

Это убрало бы эти уродливые преобразования повсюду - и действительно, вам действительно нужен доступ к DataReader в этой лямбде?

Я проиллюстрирую это кодом для ваших примеров.
Престижность Дэвиду Харкнессу за идею цепочки.

var tuples = new DataAccess ("select ProductId, AvailableQuantity from Shop.Product where ShopId = @shopId")
    .With (new { shopId = this.Shop.Id }) // map parameters to values
    .ReadMany (row =>
         Tuple.Create (row.Value<int> ("ProductId"), row.Value<int> ("AvailableQuantity"))); 

var strings = new DataAccess ("select distinct ProductName from Shop.Product")
    .ReadMany (row => row.Value<string> ("ProductName")); 

Я также вижу расширение для обработки выделения одной строки:

var productName = new DataAccess ("select ProductName from Shop.Product where ProductId = @productId")
    .With (new { productId = this.SelectedProductId }) // whatever
    .ReadOne (row => row.Value<string> ("ProductName")); 

Это черновик для Row класса:

class Row {
    DataReader reader;

    public Row (DataReader reader)
    {
        this.reader = reader;
    }

    public T Value<T> (string column)
    {
        return (T) Convert.ChangeType (reader [column], typeof (T));
    }
}

Он создается для вызовов ReadOne и ReadMany и обеспечивает удобный (и ограниченный) доступ к базовому DataReader для лямбда-селектора.

1 голос
/ 06 февраля 2011

Ваш подход к абстракции выглядит здравым. Любое снижение производительности из-за дополнительных вызовов методов будет тривиальным, а время разработки намного дороже, чем процессорное время. Когда вам нужно добавить транзакции или выборки из одной строки, вы можете расширить свои библиотечные классы. Вы хорошо используете Не повторяйте себя здесь.

Spring Framework для Java интенсивно использует эти типы шаблонных классов и помощников, таких как JdbcTemplate и HibernateTemplate, чтобы устранить необходимость для разработчиков писать шаблонный код. Идея состоит в том, чтобы написать и протестировать его один раз и многократно использовать.

1 голос
/ 06 февраля 2011

Прежде всего, никогда не извиняйтесь за то, что не вставляете код для копирования.

Ваша абстракция выглядит хорошо, однако меня больше беспокоит тот факт, что первый приведенный вами пример держит SqlConnection открытым дольшечем должно быть.

Использование IEnumerable<T> - это замечательно, поскольку вы откладываете выполнение до того, когда и если оно будет использовано.Однако, пока вы не достигли конца вашего перечисления, соединение остается открытым.

Реализация вашего метода может потреблять все перечисление через ToList(), а затем вместо этого возвращать список.Вы могли бы даже по-прежнему поддерживать отложенное выполнение, внедрив небольшой пользовательский перечислитель.

Но мне нужно предупредить об этом, убедиться, что во время перечисления старый код не совершает магию.

1 голос
/ 06 февраля 2011

Мои мысли: вы встраиваете SQL в код, в строку (в отличие от использования LINQ, который по крайней мере проверяется на синтаксис, что помогает обеспечить синхронизацию файла отображения DBML или EDMX со структурой вашей базы данных). Таким образом, встраивание SQL в код, не проверяемый на синтаксис, может легко привести к не поддерживаемому коду, когда вы (или кто-то еще) впоследствии изменяете структуру базы данных или встроенную строку SQL таким образом, что нарушает работу приложения. Встраивание SQL в строки особенно подвержено возникновению труднодоступных ошибок, поскольку код с логическими ошибками все равно будет правильно компилироваться; это повышает вероятность того, что разработчики, менее знакомые с базой кода, получат ложное представление о безопасности, поскольку внесенные ими изменения не имели каких-либо неблагоприятных или непреднамеренных последствий.

0 голосов
/ 06 февраля 2011

Для делегата, впервые знакомого с этим подходом, делегату становится немного труднее читать / понимать, но в конце концов его будет достаточно легко подобрать.

С точки зрения ремонтопригодности может быть сложнее понять трассировку стека, когда в конечном итоге появляются ошибки. Прежде всего, если в этом разделе возникает ошибка:

new DataAccess<string>.Yield(
    dataReader =>
    {
        return new Tuple<int, int>(
            (int)dataReader["ProductId"],
            Convert.ToInt32(dataReader["AvailableQuantity"]);
    }));

Использование yield налагает некоторые ограничения на то, где вы можете попытаться / поймать ( Почему доходность не может появиться внутри блока try с перехватом? ), но это проблема предыдущего подхода, так как ну и может не относиться к вашему сценарию.

...