Чи буде закрито підключення до бази даних, якщо ми отримаємо рядок читання даних і не прочитаємо всі записи?


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;
  }
}

Я спробував той самий приклад у зразку консольного додатка і під час налагодження помітив, що остаточний блок GetRecords()не виконується. Як я можу забезпечити закриття з'єднання БД? Чи є кращий спосіб, ніж використання yieldключового слова? Я намагаюся створити спеціальний клас, який буде відповідати за виконання вибраних SQL та збережених процедур у БД і повертатиме результат. Але я не хочу повертати DataReader абоненту. Також я хочу переконатися, що з'єднання буде закрито у всіх сценаріях.

Редагування Змінено відповідь на відповідь Бена, оскільки неправильно розраховувати, що користувачі методу правильно використовуватимуть метод, а щодо з'єднання з БД це буде дорожче, якщо метод викликається кілька разів без причини.

Дякую Якобу та Бену за детальне пояснення.


З того, що я бачу, ви не відкриваєте з'єднання в першу чергу
папараццо

@Frisbee Відредаговано :)
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, тому що якщо у вас є великий запит, який ви представляєте на етапі, ви можете перервати прочитане раніше на швидкість, передаючи функцію <, а не дію <>; це відчуває себе чистішим, ніж використання функції 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.

Щоб отримати більше інформації про реалізацію ітератора, ви можете прочитати цю статтю Джона Скіта

Редагувати

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

Вам слід розглянути одне з рішень, запропонованих @BenAaronson.


1
Це вкрай ненадійно. Наприклад: var records = GetRecords(); var first = records.First(); var count = records.Count()реально виконати цей метод двічі, щоразу відкриваючи та закриваючи з'єднання та зчитувач. Ітерація над ним двічі робить те саме. Ви також можете передавати результат довгий час, перш ніж насправді повторити його, тощо. Ніщо в підписі методу не передбачає такої поведінки
Бен Ааронсон,

2
@BenAaronson Я у своїй відповіді зосередився на конкретному питанні щодо поточної реалізації. Якщо ви подивитесь на всю проблему, я погоджуюся, що набагато краще використовувати спосіб, який ви запропонували у своїй відповіді.
Якуб Лорц

1
@JakubLortz Досить справедливо
Бен Аронсон

2
@EsbenSkovPedersen Зазвичай, коли ви намагаєтеся зробити щось бездоганним, ви втрачаєте певну функціональність. Вам потрібно збалансувати це. Тут ми не дуже втрачаємо, охоче завантажуючи результати. У всякому разі, найбільша проблема для мене - це називання. Коли я бачу метод, який називається GetRecordsповерненням IEnumerable, я очікую, що він завантажить записи в пам'ять. З іншого боку, якби це була властивість Records, я вважаю, що це якась абстракція над базою даних.
Якуб Лорц

1
@EsbenSkovPedersen Ну, дивіться мої коментарі до відповіді nvoigt. У мене взагалі немає проблем із методами повернення IEnumerable, який зробить значну роботу при повторному повторенні, але в цьому випадку він, здається, не відповідає наслідкам підпису методу
Бен Аронсон

5

Незалежно від конкретної yieldповедінки, ваш код містить шляхи виконання, які не будуть розміщувати ваші ресурси правильно. Що робити, якщо ваш другий рядок кидає виняток чи ваш третій? Або навіть ваш четвертий? Вам знадобиться дуже складний ланцюжок спробувати / нарешті, або ви можете використовувати 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 є функція, яка називається статичним аналізом коду. Ви можете використовувати його, щоб дізнатися, чи правильно ви розмістили ресурси.


Мова дозволяє повторювати 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.

Фактично, передбачення передбачає багато чого в інтернаті.

Перевірте /programming/13459447/do-i-need-to-consider-disposed-of-any-ienumerablet-i-use

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.