SqlDataReaderをCloseしなくても例外が発生しない

タグの編集
投稿者 MMTRS  (社会人) 投稿日時 2010/6/22 21:08:13
SqlConnectionとSqlDataReaderについての質問です。

通常、SqlConnectionの接続文字列にMultipleActiveResultSets(MARS)について特に記述しなければ、
MARSはOFF(False)となって、同じ接続で複数のSqlDataReaderを同時に取得することはできませんよね?

ところが今開発中のプログラムで、SqlDataReaderをCloseしていないのに同じ接続で
新しくSqlDataReaderを取得できてしまっています。
正確には、同じ処理を繰り返し行っていると何度目かにようやく例外が発生します。

処理の流れは下の通りです。

1.TextBox1に文字を入力
2.入力された値を元にデータベースからデータを取得(←ここでCloseしていない)
3.TextBox2に文字を入力
4.入力された値を元にデータベースからデータを取得(←ここではCloseしている)

1~4を繰り返していると4でデータを取得しようとした時に
「このコマンドに関連付けられている DataReader が既に開かれています。~」
の例外が発生します。

SqlConnectionはモジュールレベルで画面表示時にOpenしています。
SqlDataReaderは2,4どちらもプロシージャで宣言しています。

考えられる原因と対処法など教えていただければ幸いです。
投稿者 流れ者  (社会人) 投稿日時 2010/6/23 08:20:00
おはようございます。

>考えられる原因と対処法など教えていただければ幸いです。

公開できる範囲でソースを提示していただけると、他の皆様も回答しやすいと思います。

投稿者 MMTRS  (社会人) 投稿日時 2010/6/23 09:19:01
>公開できる範囲でソースを提示していただけると、他の皆様も回答しやすいと思います。
そうですね、失礼しました。簡略化したものですが処理の流れは以下のようになっています。

Public Class Form1

    Private cn As New SqlClient.SqlConnection
    Private cm As SqlClient.SqlCommand

    Private Sub Form1_Load(ByVal sender As ObjectByVal e As System.EventArgs) Handles Me.Load

        cn.ConnectionString = 接続文字列
        cn.Open()
        cm = cn.CreateCommand()

    End Sub

    Private Sub TextBox1_Validating(ByVal sender As ObjectByVal e As System.ComponentModel.CancelEventArgs) Handles TextBox1.Validating

        Call GetData1()     'Closeなし 

    End Sub

    Private Sub TextBox2_Validating(ByVal sender As ObjectByVal e As System.ComponentModel.CancelEventArgs) Handles TextBox2.Validating

        Call GetData2()     'Closeあり 

    End Sub

    Private Sub GetData1()
        Dim dr As SqlClient.SqlDataReader

        cm.CommandText = "SELECT * FROM [テーブル1] WHERE [フィールド1] = '" & TextBox1.Text & "'"
        dr = cm.ExecuteReader()

        If dr.Read() Then
            Label1.Text = dr("フィールド2")
        End If

        'dr.Close() 

    End Sub

    Private Sub GetData2()
        Dim dr As SqlClient.SqlDataReader

        cm.CommandText = "SELECT * FROM [テーブル2] WHERE [フィールド1] = '" & TextBox2.Text & "'"
        dr = cm.ExecuteReader()

        If dr.Read() Then
            Label1.Text = dr("フィールド2")
        End If

        dr.Close()

    End Sub
End Class


ただ、上のコードを実行しても初回のGetData1→GetData2で必ずエラーとなります。
実際のコードは他にもcnを使用してSqlDataReaderを取得している箇所はありますが
問題となっている現象は上の処理と同じ流れで発生します。

それからもう1つ書き忘れましたが、VB2008、SQL Server 2008です。
投稿者 MMTRS  (社会人) 投稿日時 2010/6/24 19:52:47
いろいろ調べていましたがなかなか原因等わからないので、
MSDNフォーラムにも同内容で投稿させて頂きました。
こちらの掲示板も引き続き確認しますので、宜しくお願いします。
投稿者 よねKEN  (社会人) 投稿日時 2010/6/24 20:32:11
> ところが今開発中のプログラムで、SqlDataReaderをCloseしていないのに同じ接続で
> 新しくSqlDataReaderを取得できてしまっています。

GetData1メソッドの変数drはローカル変数ですから、
メソッドを抜けた時点で参照がなくなります。
このdrは保持していたSqlDataReaderインスタンスはいずれガベージコレクターに回収されます。

ガベージコレクターが動作し、このインスタンスが回収された場合、
回収作業の中で、Disposeメソッドが呼び出され、SqlDataReaderインスタンスはクローズされます。

ですので、明示的にCloseメソッドを呼び出していないような「好ましくない」実装がされていても、
必ずしも例外が発生するとは限りません。このような理由が一つの可能性としては考えられます。

GetData1メソッド、GetData2メソッドを連続で呼び出したような場合だと
GetData2メソッドの呼び出し時点でGetData1メソッドのローカル変数drで参照していた
インスタンスはまだ回収されていない可能性が高いので例外は発生すると思います。
このように連続してGetData1メソッド、GetData2メソッドを呼び出したような場合でも
例外が発生しないのであれば、ガベージコレクターが動作してクローズされた、
という原因ではないかもしれません。
投稿者 MMTRS  (社会人) 投稿日時 2010/6/25 09:36:03
よねKENさんありがとうございます。

>ガベージコレクターが動作し、このインスタンスが回収された場合、
>回収作業の中で、Disposeメソッドが呼び出され、SqlDataReaderインスタンスはクローズされます。
これについてですが、先のコードを一部変更して、

    Private Sub TextBox1_Validating(ByVal sender As ObjectByVal e As System.ComponentModel.CancelEventArgs) Handles TextBox1.Validating

        Call GetData1()     'Closeなし 
        Call Dummy()
        Call GetData2()     'Closeあり 

    End Sub

    Private Sub Dummy()
        Dim dmAry() As String
        Dim i As Integer

        For i = 0 To 9999   '←適当な値 
            ReDim Preserve dmAry(i)
            dmAry(i) = i.ToString()
        Next

    End Sub



として、無駄にメモリを使用するようなDummyメソッドを追加してみたところ、
Dummyメソッドの有無によって、GetData2で例外が発生したりしなかったりという状況になりました。

これはつまり、Dummyメソッドでメモリを多く使用することで、GetData1のdrに対して
ガベージコレクターが動作したということになるのでしょうか。
投稿者 よねKEN  (社会人) 投稿日時 2010/6/25 11:21:31
> これはつまり、Dummyメソッドでメモリを多く使用することで、GetData1のdrに対して
> ガベージコレクターが動作したということになるのでしょうか。

その可能性は高そうですね。もう少し確実な検証をするには、
GC.Collectで明示的にガベージコレククションを起動してみるとか。

Call GetData1()     'Closeなし 
Call GetData2()     'Closeあり 



Call GetData1()     'Closeなし 
GC.Collect() 
GC.WaitForPendingFinalizers()  ' Finalize≒Dispose≒Closeの処理完了を確実に待つため
Call GetData2()     'Closeあり 

とで比較するとよいと思います。

投稿者 MMTRS  (社会人) 投稿日時 2010/6/25 12:00:14
よねKENさんの言うとおり、

Call GetData1()     'Closeなし
Call GetData2()     'Closeあり

では100%例外が発生し、

Call GetData1()     'Closeなし 
GC.Collect() 
GC.WaitForPendingFinalizers()  ' Finalize≒Dispose≒Closeの処理完了を確実に待つため
Call GetData2()     'Closeあり

では例外が発生しませんでした。
(ガベージコレクターに関しては今回初めて知りました。)

ということは、SqlConnectionの重複使用による例外を回避するには

1.接続文字列のオプションでMARSを有効にする
2.SqlDataReaderの取得前にガベージコレクションを強制実行する

の2通りの方法で問題ないでしょうか。
もちろん、上記は本来作りたかったものとは異なる部分もあるでしょうし、
再度コードを見直して他にもSqlDataReaderのCloseが抜けている箇所がないか
確認するというのは前提として。
投稿者 よねKEN  (社会人) 投稿日時 2010/6/25 12:57:51
> ということは、SqlConnectionの重複使用による例外を回避するには

> 1.接続文字列のオプションでMARSを有効にする
> 2.SqlDataReaderの取得前にガベージコレクションを強制実行する

> の2通りの方法で問題ないでしょうか。

1.については私はよくわかりませんが、2.はやらないでください。
先ほどの検証コードは現象の理由を明らかにするために示しただけですので、
あくまできちんとCloseするのが正等な解決策です。

それをせずに「2.SqlDataReaderの取得前にガベージコレクションを強制実行する」とするのは
単なるごまかしですので、そういう目的でGC.Collectなどを使うべきではありません。

> 再度コードを見直して他にもSqlDataReaderのCloseが抜けている箇所がないか
> 確認するというのは前提として。 

と書かれているので、上記のような指摘は理解されているとは思いますが、念のため。

後、確実なCloseのために、VB2005以降であればUsing ~ End Usingの使用も検討してください。
投稿者 MMTRS  (社会人) 投稿日時 2010/6/25 13:42:31
> 1.については私はよくわかりませんが、2.はやらないでください。
> 先ほどの検証コードは現象の理由を明らかにするために示しただけですので、
> あくまできちんとCloseするのが正等な解決策です。
そうですよね・・・了解しました。

> 後、確実なCloseのために、VB2005以降であればUsing ~ End Usingの使用も検討してください。
Using ~ End Usingについては、そういうものがあることは知っていましたが今まで使っていませんでした。

確かに、GetData1、GetData2でのdrをUsingで使用した場合にも例外は確実に発生しなくなります。
Usingで宣言したものはEnd Usingで必ず破棄(Dispose?Finalize?)されるからですよね?

今後プログラムの変更等の可能性を考えれば、Using ~ End Usingを使用しつつ
必ずCloseするというのが1番確実で、パフォーマンス的にもメンテナンス効率的にも良さそうですね。

よねKENさん、それから流れ星さんも、どうもありがとうございました。