データリーダー行を取得し、すべてのレコードを読み取らない場合、データベース接続は閉じられますか?


15

方法を理解しながらyieldキーワード作品を、私が出会ったリンク1リンク2の使用を提唱StackOverflowの上yield returnDataReaderオブジェクトを反復処理しながら、をし、それは私のニーズに合ったとしても。しかし、yield return以下に示すように使用し、DataReader全体を反復処理しない場合、DB接続はいつまでも開いたままになるのか、疑問に思います。

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()実行されないことに気付きました。DB接続の終了を確認するにはどうすればよいですか?yieldキーワードを使用するよりも良い方法はありますか?DBで選択SQLとストアドプロシージャを実行し、結果を返すカスタムクラスを設計しようとしています。しかし、DataReaderを呼び出し元に返したくありません。また、すべてのシナリオで接続が閉じられるようにします。

編集メソッドの呼び出し元がメソッドを正しく使用することを期待するのは間違っているため、Benの答えへの答えを変更しました。

詳細な説明をしてくれたJakobとBenに感謝します。


私が見るところからあなたはそもそも接続を開いていない
パパラッチ

@Frisbee編集:)
GawdePrasad

回答:


12

はい、説明した問題に直面します。結果の反復処理が完了するまで、接続を開いたままにします。これに対処するために考えられる2つの一般的なアプローチがあります:

プッシュ、プルしないでください

現在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という名前のアプローチを使用しています。SQLCommandのDatareaderが複数の出力(myReader.NextResult()など)を返し、リストのリストを作成すると、メモリオーバーヘッドになりますか?または、データリーダーを呼び出し元に送信すること(個人的にはそれを好まない)は、非常に効率的ですか?[ただし、接続はより長く開きます]。ここで正確に混乱しているのは、リストのリストを作成するよりも接続を長く開いておくというトレードオフの間です。DBに接続する私のWebページに500人以上の人々がアクセスすると想定しても安全です。
-GawdePrasad

1
@GawdePrasadそれは、呼び出し側がリーダーに対して何をするかによります。コレクション自体にアイテムを配置するだけであれば、大きなオーバーヘッドはありません。多くの値をメモリに保持する必要のない操作を行うと、メモリのオーバーヘッドが発生します。ただし、非常に大量に保存している場合を除き、おそらく気にする必要があるオーバーヘッドではありません。いずれにせよ、大きな時間のオーバーヘッドはないはずです。
ベンアーロンソン

「プッシュ、プルしない」アプローチは、「結果を繰り返し処理するまで、接続を開いたままにしておく」問題をどのように解決しますか?このアプローチでも繰り返しを完了するまで、接続は開いたままです。
BornToCode

1
@BornToCode「次のようにyield returnを使用し、DataReader全体を反復処理しない場合、DB接続は永久に開いたままになります」という質問を参照してください。問題は、IEnumerableを返すことであり、それを処理するすべての呼び出し元は、この特別な落とし穴に注意する必要があります。「繰り返し処理を完了しない場合、接続を開いたままにします」。pushメソッドでは、その責任を呼び出し元から引き離します。部分的に反復するオプションはありません。制御が返されるまでに、接続は閉じられます。
ベンアーロンソン

1
これは素晴らしい答えです。プッシュアプ​​ローチが好きです。ページ単位で表示している大きなクエリがある場合、Action <>ではなくFunc <>を渡すことで、速度を上げるために読み取りを早く中止できます。これは、GetRecords関数にページングを実行させるよりもクリーンに感じます。
ヘルカホリズム

10

あなたのfinallyブロックは常に実行されます。

あなたが使用する場合yield return、コンパイラは、ステートマシンを実装するために新しいネストされたクラスを作成します。

このクラスには、finallyブロックからのすべてのコードが個別のメソッドとして含まれます。finally状態に応じて実行する必要があるブロックを追跡します。必要なすべてのfinallyブロックがDisposemethod で実行されます。

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つを検討する必要があります。


1
これは非常に信頼できません。たとえば、var records = GetRecords(); var first = records.First(); var count = records.Count()実際にそのメソッドを2回実行し、そのたびに接続とリーダーを開閉します。それを2回繰り返しても同じことをします。また、メソッドのシグネチャでは何も行動のようなものを意味していないなど、実際にそれを反復する前に長い時間のために周りに結果を渡すかもしれない
ベン・アーロンソン

2
@BenAaronson私の答えでは、現在の実装に関する特定の質問に焦点を当てました。問題全体を見ると、答えで提案した方法を使用する方がはるかに良いことに同意します。
ジャクブロルツ

1
@JakubLortzまあまあ
ベンアーロンソン

2
@EsbenSkovPedersen通常、馬鹿なことをしようとすると、いくつかの機能が失われます。バランスを取る必要があります。ここでは、結果を熱心にロードすることで大きな損失はありません。とにかく、私にとって最大の問題はネーミングです。私は名前のメソッドを参照するときGetRecordsの復帰をIEnumerable、私はそれがメモリにレコードをロードするために期待しています。一方、それがプロパティである場合Records、私はむしろデータベース上のある種の抽象化であると仮定します。
ジャクブローツ

1
@EsbenSkovPedersenまあ、nvoigtの答えに対する私のコメントを見てください。私は、メソッドが繰り返し処理時に重要な仕事を行いますIEnumerableをを返すと、一般的には問題ありませんが、この場合には、メソッドのシグネチャの意味合いを合わせていないようだ
ベン・アーロンソン

5

特定のyield動作に関係なく、コードにはリソースを正しく破棄しない実行パスが含まれています。2行目が例外をスローした場合、または3行目が例外をスローした場合はどうなりますか?それとも4番目ですか?非常に複雑な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;
               }
            }
        }
    }
}

これは直観的ではなく、メソッドを呼び出す人は、結果を2回列挙するとメソッドが2回呼び出されることを知らないかもしれないと人々は言っています。まあ、タフな運。それが言語の仕組みです。それがIEnumerable<T>送信する信号です。これは戻らない理由があるList<T>かがT[]。これを知らない人々は教育を受け、回避する必要はありません。

Visual Studioには、静的コード分析と呼ばれる機能があります。リソースを適切に破棄したかどうかを調べるために使用できます。


この言語ではIEnumerable、まったく関係のない例外をスローしたり、5GBファイルをディスクに書き込んだりするなど、あらゆる種類の処理を繰り返すことができます。言語がそれを許可IEnumerableしているからといって、それが起こるという兆候を与えないメソッドからそれを返すことを受け入れることができるという意味ではありません。
ベンアーロンソン

1
anを返すことIEnumerable<T> 、それを2回列挙すると2回の呼び出しになることを示しています。enumerableを複数回列挙すると、ツールに役立つ警告が表示されます。例外のスローに関するポイントは、戻り値のタイプに関係なく等しく有効です。そのメソッドがを返す場合List<T>、同じ例外がスローされます。
nvoigt

私はあなたのポイントを理解していますが、ある程度は同意します-もしあなたがあなたにIEnumerable当時の警告者を与える方法を使っているなら しかし、メソッドライターとして、あなたは自分の署名が意味するものを守る責任があると思います。メソッドが呼び出されますGetRecords。そのため、返されるときに、レコードを取得することが期待されます。それが呼び出された場合GiveMeSomethingICanPullRecordsFrom(または、より現実的にGetRecordSourceまたは類似した場合)、遅延IEnumerableがより受け入れられます。
ベンアーロンソン

@BenAaronson私は、例えばことに同意しFileReadLinesかつReadAllLines簡単に離れて告げ、それは良いことだことができます。(ただし、メソッドのオーバーロードは戻り値の型のみで異なることはできないという事実のためです)。おそらく、ReadRecordsとReadAllRecordsが命名スキームとしてここに適合するでしょう。
nvoigt

2

あなたがしたことは間違っているように見えますが、うまくいきます。

IDisposableも継承するため、IEnumerator <>を使用する必要があります。

ただし、foreachを使用しているため、コンパイラはまだIDisposableを使用してforeachのIEnumerator <>を生成しています。

実際、foreachは内部的に多くのことを伴います。

/programming/13459447/do-i-need-to-consider-disposing-of-any-ienumerablet-i-useを確認してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.