Будет ли соединение с базой данных закрыто, если мы выдадим строку заголовка данных и не прочитаем все записи?

15

Понимая, как yieldработает ключевое слово, я наткнулся на link1 и link2 в StackOverflow, который поддерживает использование yield returnитерации по DataReader, и это также удовлетворяет мои потребности. Но меня удивляет, что произойдет, если я буду использовать, yield returnкак показано ниже, и если я не буду перебирать весь DataReader, будет ли соединение с БД оставаться открытым навсегда?

IEnumerable<IDataRecord> GetRecords()
{
    SqlConnection myConnection = new SqlConnection(@"...");
    SqlCommand myCommand = new SqlCommand(@"...", myConnection);
    myCommand.CommandType = System.Data.CommandType.Text;
    myConnection.Open();
    myReader = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
    try
       {               
          while (myReader.Read())
          {
            yield return myReader;          
          }
        }
    finally
        {
            myReader.Close();
        }
}


void AnotherMethod()
{
    foreach(var rec in GetRecords())
    {
       i++;
       System.Console.WriteLine(rec.GetString(1));
       if (i == 5)
         break;
  }
}

Я попробовал тот же пример в образце консольного приложения и заметил при отладке, что блок finally GetRecords()не выполняется. Как я могу обеспечить закрытие соединения с БД? Есть ли лучший способ, чем использовать yieldключевое слово? Я пытаюсь разработать собственный класс, который будет отвечать за выполнение отдельных SQL и хранимых процедур в БД и будет возвращать результат. Но я не хочу возвращать DataReader вызывающей стороне. Также я хочу убедиться, что соединение будет закрыто во всех сценариях.

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

Спасибо Якобу и Бену за подробное объяснение.

GawdePrasad
источник
Из того, что я вижу, вы не открываете соединение в первую очередь
папараццо
@ Фрисби Отредактировано :)
GawdePrasad

Ответы:

12

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

Тяни, не тяни

В настоящее время вы возвращаете IEnumerable<IDataRecord>структуру данных, из которой вы можете извлечь. Вместо этого вы можете переключить свой метод, чтобы вытолкнуть его результаты. Самый простой способ - передать значение, Action<IDataRecord>которое вызывается на каждой итерации:

void GetRecords(Action<IDataRecord> callback)
{
    // ...
      while (myReader.Read())
      {
        callback(myReader);
      }
}

Обратите внимание, что, учитывая, что вы имеете дело с набором элементов, IObservable/ IObserverможет быть немного более подходящей структурой данных, но если вам это не нужно, простое Actionгораздо проще.

Охотно оценивать

Альтернативой является просто убедиться, что итерация полностью завершена перед возвратом.

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

IEnumerable<T> GetRecords<T>(Func<IDataRecord,T> extractor)
{
    // ...
     var result = new List<T>();
     try
     {
       while (myReader.Read())
       {
         result.Add(extractor(myReader));         
       }
     }
     finally
     {
         myReader.Close();
     }
     return result;
}
Бен Ааронсон
источник
Я использую подход, который вы назвали Eagerly Evaluate. Теперь это будет нехватка памяти, если Datareader моего SQLCommand возвращает несколько выходных данных (например, myReader.NextResult ()) и я создаю список из списка? Или отправка считывателя данных вызывающей стороне (лично мне это не нравится) будет намного эффективнее? [но связь будет открыта дольше]. Что меня здесь смущает, так это компромисс между поддержанием открытого соединения дольше, чем создание списка списков. Вы можете с уверенностью предположить, что около 500+ человек посетят мою веб-страницу, которая подключается к БД.
GawdePrasad
1
@GawdePrasad Это зависит от того, что звонящий будет делать с читателем. Если бы он просто помещал предметы в саму коллекцию, значительных накладных расходов не было бы. Если бы он выполнял операцию, которая не требует сохранения всей массы значений в памяти, то это приводит к перегрузке памяти. Однако, если вы не храните очень большие суммы, вам, вероятно, не о чем беспокоиться. В любом случае, не должно быть значительных временных затрат.
Бен Ааронсон
Каким образом подход «нажми, не тяни» решает проблему «пока не закончишь итерацию по результату, ты будешь держать соединение открытым»? Соединение все еще остается открытым, пока вы не закончите итерацию даже при таком подходе, не так ли?
BornToCode
1
@BornToCode См. Вопрос: «Если я использую возвращение доходности, как показано ниже, и если я не перебираю весь DataReader, будет ли соединение с БД оставаться открытым всегда». Проблема в том, что вы возвращаете IEnumerable, и каждый вызывающий его пользователь должен знать об этом специальном уловке: «Если вы не закончите итерацию по мне, я оставлю открытым соединение». В методе push вы снимаете эту ответственность с вызывающей стороны - у них нет возможности частично выполнить итерацию. К тому времени, когда контроль возвращается им, соединение закрывается.
Бен Ааронсон
1
Это отличный ответ. Мне нравится подход push, потому что если у вас есть большой запрос, который вы представляете постраничным способом, вы можете прервать чтение раньше для скорости, передав Func <>, а не Action <>; это кажется чище, чем заставить функцию GetRecords также делать пейджинг.
Whelkaholism
10

Ваш finallyблок всегда будет выполняться.

При использовании yield returnкомпилятор создаст новый вложенный класс для реализации конечного автомата.

Этот класс будет содержать весь код из finallyблоков как отдельные методы. Он будет отслеживать finallyблоки, которые должны быть выполнены в зависимости от состояния. Все необходимые finallyблоки будут выполнены в Disposeметоде.

Согласно спецификации языка C #, foreach (V v in x) embedded-statementрасширен до

{
    E e = ((C)(x)).GetEnumerator();
    try
    {
        while (e.MoveNext())
        {
            V v = (V)(T)e.Current;
            embedded - statement
        }
    }
    finally {
         // Dispose e
    }
}

поэтому перечислитель будет удален, даже если вы выйдете из цикла с помощью breakили return.

Чтобы получить больше информации о реализации итератора, вы можете прочитать эту статью от Jon Skeet

редактировать

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

Вы должны рассмотреть одно из решений, предложенных @BenAaronson.

Якуб Лортц
источник
1
Это крайне ненадежно. Например: var records = GetRecords(); var first = records.First(); var count = records.Count()фактически выполнит этот метод дважды, открывая и закрывая соединение и читатель каждый раз. Итерирование дважды делает одно и то же. Вы также можете долго передавать результат, прежде чем фактически итерировать его и т. Д. Ничто в сигнатуре метода не подразумевает такого поведения
Бен Ааронсон
2
@BenAaronson В своем ответе я сосредоточился на конкретном вопросе о текущей реализации. Если вы посмотрите на всю проблему, я согласен, что гораздо лучше использовать способ, который вы предложили в своем ответе.
Якуб Лорц
1
@JakubLortz Достаточно справедливо
Бен Ааронсон
2
@EsbenSkovPedersen Обычно, когда вы пытаетесь сделать что-то идиотостойкое, вы теряете некоторую функциональность. Вы должны сбалансировать это. Здесь мы не теряем много, жадно загружая результаты. В любом случае, самая большая проблема для меня - это названия. Когда я вижу метод с именем GetRecordsreturning IEnumerable, я ожидаю, что он загрузит записи в память. С другой стороны, если бы это было свойство Records, я бы предпочел предположить, что это какая-то абстракция над базой данных.
Якуб Лорц
1
@EsbenSkovPedersen Ну, смотрите мои комментарии к ответу nvoigt. В общем, у меня нет проблем с методами, возвращающими IEnumerable, который будет выполнять значительную работу при повторении, но в этом случае он, кажется, не соответствует последствиям сигнатуры метода
Бен Ааронсон,
5

Независимо от конкретного yieldповедения, ваш код содержит пути выполнения, которые не будут правильно распоряжаться вашими ресурсами. Что если ваша вторая строка выдает исключение или вашу третью? Или даже ваш четвертый? Вам понадобится очень сложная цепочка try / finally, или вы можете использовать usingблоки.

IEnumerable<IDataRecord> GetRecords()
{
    using(var connection = new SqlConnection(@"..."))
    {
        connection.Open();

        using(var command = new SqlCommand(@"...", connection);
        {
            using(var reader = command.ExecuteReader())
            {
               while(reader.Read())
               {
                   // your code here.
                   yield return reader;
               }
            }
        }
    }
}

Люди отметили, что это не интуитивно понятно, что люди, вызывающие ваш метод, могут не знать, что при перечислении результата дважды вызовет метод дважды. Ну, удачи. Так работает язык. Это сигнал, который IEnumerable<T>посылает. Есть причина, по которой это не возвращается List<T>или T[]. Люди, которые не знают этого, должны быть образованными, а не работать вокруг.

Visual Studio имеет функцию под названием статический анализ кода. Вы можете использовать его, чтобы узнать, правильно ли вы распорядились ресурсами.

nvoigt
источник
Язык позволяет перебирать IEnumerableи делать все что угодно, например, выбрасывать совершенно не связанные исключения или записывать файлы 5 ГБ на диск. Тот факт, что язык допускает это, не означает, что он может возвращать IEnumerableметод, который делает это, из метода, который не указывает, что это произойдет.
Бен Ааронсон
1
Возвращение IEnumerable<T> является признаком того, что его двойное перечисление приведет к двум вызовам. Вы даже получите полезные предупреждения в своих инструментах, если перечислите их несколько раз. Пункт о создании исключений одинаково действителен независимо от типа возвращаемого значения. Если этот метод вернет a, List<T>он выдаст те же исключения.
nvoigt
Я понимаю вашу точку зрения и в какой-то степени я согласен - если вы используете метод, который дает вам IEnumerableтогдашний предлог. Но я также думаю, что как автор метода вы обязаны придерживаться того, что подразумевает ваша подпись. Метод вызывается GetRecords, поэтому, когда он вернется, вы ожидаете, что он получит записи . Если бы это назвали GiveMeSomethingICanPullRecordsFrom(или, более реалистично GetRecordSourceили подобный), тогда ленивый IEnumerableбыл бы более приемлемым.
Бен Ааронсон
@BenAaronson Я согласен, что, например File, ReadLinesи ReadAllLinesможет быть легко сказано отдельно, и это хорошо. (Хотя это больше из-за того, что перегрузка метода не может отличаться только типом возвращаемого значения). Может быть, ReadRecords и ReadAllRecords подойдут здесь в качестве схемы именования.
nvoigt
2

То, что вы сделали, кажется неправильным, но будет работать нормально.

Вы должны использовать IEnumerator <> , так как он также наследует IDisposable .

Но поскольку вы используете foreach, компилятор все еще использует IDisposable для генерации IEnumerator <> для foreach.

на самом деле foreach включает в себя многое изнутри.

Проверьте /programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-use

Avlin
источник