none
DataTable.Select の結果がおかしい

    質問

  • DataTable.Select の結果が思うように動作しなかったため調べたところ、どうもバグがあるようです。
    再現できる最小のコードを作成しました。
    (言語には依存しないと思いますが、コードが C# なのでこちらに)

    このコードでは DataView による絞り込みと Select の件数を表示していますが、同じ条件であるにもかかわらず、件数が違ってしまいます。Select の方が少なかったり多かったりし、まったく正常に機能していません。この再現コードではゼロになることが多いですが、列数やデータ内容によって変わります。

    みなさんの環境でも発生しますよね?
    どなたか試していただけませんでしょうか?
    私の環境は Visual Studio 2008、.NET Framework 3.5 ですが、2.0 でも同じでした。
    (Visual Studio 2010 Bata1、.NET Frame 4.0 でも同じでした)

    private void button1_Click(object sender, EventArgs e)
    {
        var dt = new DataTable();

        dt.Columns.Add("Column1", typeof(string)).AllowDBNull = false;
        dt.Columns.Add("Column2", typeof(string)).AllowDBNull = false;
        dt.Columns.Add("Column3", typeof(string)).AllowDBNull = false;

        // DataTable.Load などでは内部的に実行される
        dt.BeginLoadData();
        dt.EndLoadData();

        // (回避その1 → 削除しない)
        dt.Columns.Remove("Column1");

        // (回避その2 → テーブルを作り直す)
        //dt = dt.Copy();

        var r = new Random();
        for (var i = 0; i < 1000; i++)
        {
            var row = dt.NewRow();
            row["Column2"] = "";
            row["Column3"] = new string((char)r.Next('A', 'Z' + 1), 1);
            dt.Rows.Add(row);
        }

        // DataView による絞り込み
        var rowCount1 = new DataView(dt) { RowFilter = "Column3 = 'C'" }.Count;
        // Select による絞り込み
        var rowCount2 = dt.Select("Column3 = 'C'").Length;
        // (回避その3 → 条件内で関数を使う)
        var rowCount3 = dt.Select("convert(Column3, 'System.String') = 'C'").Length;

        MessageBox.Show(
            string.Format(
                "1. DataView.RowFilter → {0}\r\n" +
                "2. DataTable.Select → {1}\r\n" +
                "3. DataTable.Select(2) → {2}",
                rowCount1, rowCount2, rowCount3));
    }

    再現条件のポイントは、
    ・ Select で参照する列より前に AllowDBNull が false の列がある。
    ・ BeginLoadData+EndLoadData を実行する。
    ・ Select で参照する列よりも前にある列を削除する。
    です。
    BeginLoadData+EndLoadData は、DataTable.Load メソッドなど、データベースからデータを取得する .NET Framework のメソッドの内部でも実行されるため、何らかの方法でデータベースから取得した DataTable に対して DataColumn の削除を行う操作はほとんどの場合、この問題を引き起こすことになると思います(普通は列の削除なんてしませんけど)。

    それと、Select の実行にはさらに2つの問題もあり、本件と合わせて以下の3つの症状が出ました。

    1. エラーは発生しないが、Select 結果はおかしい。
    2. 上記再現コードを何度も実行していると、たまに次のエラーが発生する。
       … エラー:Range オブジェクトの Min (??) は、 max (-1) 以下でなければなりません。
    3. 上記再現コードで "Column2" の AllowDBNull を true にすると、次のエラーが発生する。
       … エラー:インデックスが配列の境界外です。

    2 のエラーは先日のスレッド「VB2005 .NET2.0 DataTable.Select()メソッドに関して」と同じエラーですね。
    3 は Connect を "DataTable Select" で検索すると似た内容の報告はいくつかありました。たとえば「Bug in DataTable internal Index management which results in crash "Index out of bounds"」などです。

    回避策をいくつか考えてコード内に記述しましたが、他の回避策など含め、アドバイスなどありましたらお願いします。

    2009年10月5日 5:30

回答

  • 非常に興味深い問題でしたので、色々と試してみました。

    環境は、Visual Studio 2005、.NET Framework 2.0 です。

    たしかに、上記のソースを 実行
    (Visual Studio 2005ですので、varの部分はそれぞれ型指定の修正をしました。)
    したところ、同様の問題が発生しました。

    ここで、気になったのが、EndLoadData() ですが、この場合、DataTableにAddするループ処理の後ではないでしょうか?
    (データベースに接続した時も恐らくそうだと思います)

    そして、列を削除する処理はEndLoadData()の後ではなく、BeginLoadData()実行中に行うと、問題なく処理されるようです。

    データベースからデータを取得し、列を削除する場合は、列を削除する前にBeginLoadData()を行い、削除したのちEndLoadData()処理することで回避できるのではないかと思います。
    • 回答としてマーク TH01 2009年10月28日 12:38
    2009年10月5日 10:19

すべての返信

  • 非常に興味深い問題でしたので、色々と試してみました。

    環境は、Visual Studio 2005、.NET Framework 2.0 です。

    たしかに、上記のソースを 実行
    (Visual Studio 2005ですので、varの部分はそれぞれ型指定の修正をしました。)
    したところ、同様の問題が発生しました。

    ここで、気になったのが、EndLoadData() ですが、この場合、DataTableにAddするループ処理の後ではないでしょうか?
    (データベースに接続した時も恐らくそうだと思います)

    そして、列を削除する処理はEndLoadData()の後ではなく、BeginLoadData()実行中に行うと、問題なく処理されるようです。

    データベースからデータを取得し、列を削除する場合は、列を削除する前にBeginLoadData()を行い、削除したのちEndLoadData()処理することで回避できるのではないかと思います。
    • 回答としてマーク TH01 2009年10月28日 12:38
    2009年10月5日 10:19
  • ご確認ありがとうございました。


    ここで、気になったのが、EndLoadData() ですが、この場合、DataTableにAddするループ処理の後ではないでしょうか?
    (データベースに接続した時も恐らくそうだと思います)


    ご指摘いただいて気づきましたが、上記再現コードは一見すると奇妙なコードになってますね。
    この点は、確かにおっしゃる通りです。
    たとえ再現コードであったとしても、データ追加処理は BeginLoadData ~ EndLoadData の間に入れた方が素直ですね。

    なぜ上記のような再現コードにしたかといいますと、今回作成していたプログラムでは

    1. DataTable.Load(IDataReader) … スキーマおよびデータが格納される
    2. 列の削除
    3. DataTable.Select

    という処理を行った際に Select で不具合が発生したのですが、再現条件を突き詰めていくと、DataTable へのデータの格納方法はどんな方法でもよく、BeginLoadData と EndLoadData の組み合わせの後に列を削除することが最低限の再現条件であることが分かり、あのようなコードになりました。
    データがないと不具合が確認できませんので、不具合を引き起こす条件を整えた後の位置に、データを格納するコードを入れました。

    DataTable.Load と同様に Framework の内部で EndLoadData までがセットになっている状況として他には、Visual Studio のウィザードを使って型付 DataSet を使用した場合の、

    1. Form1_Load に TableAdapter.Fill のコードがウィザードによって追加される。
    2. 列の削除を行う
    3. DataTable.Select

    という状況でも同じ不具合が発生します。
    これは、TableAdapter.Fill の内部でも BeginLoadData と EndLoadData に相当するコードが実行されるからだと思います。

    データベースからデータを取得し、列を削除する場合は、列を削除する前にBeginLoadData()を行い、削除したのちEndLoadData()処理することで回避できるのではないかと思います。

    これは投稿前に試していたのですが、.NET Framework の内部などのどこかで BeginLoadData+EndLoadData の組み合わせに相当するコードが実行されると、そのあとの列の削除処理を Begin と End の間で行っても、もう遅いようです。

    想像ですが、初回の BeginLoadData+EndLoadData の時点で内部的に列の情報などがメモリ上に構築され、そのあとに行う列の変更操作によってその情報に不整合が生じてしまうのだろうと考えています。
    2009年10月5日 12:44
  • この件、コネクトでフィードバックしてきました。
    重複や後回し等になる可能性が高いと思っていますが、もしよろしければ投票をお願いします。

    「DataColumn の削除を行うと DataTable.Select の結果がおかしくなる」
    http://connect.microsoft.com/VisualStudioJapan/feedback/ViewFeedback.aspx?FeedbackID=504995

    このスレッドでどなたかに再現報告をいただいたらすぐに報告するつもりだったのですが、今頃になってしまいました。
    dw5 さんにすぐにご確認いただきましたのに失礼しました。
    ※ それと、フォーラムは「Visual Studio フィードバック」の方が良かったと後悔しています。

    また、Maboroshi さんのもうひとつの現象についても、原因は同じかもしれませんが、ついでに報告しました。

    「DataTable.Select の条件式での型推論による弊害」
    http://connect.microsoft.com/VisualStudioJapan/feedback/ViewFeedback.aspx?FeedbackID=504999

    既知の問題である可能性が高いことや、開発者が式の書き方を注意することで容易に回避できる(もしくは遭遇しない)ことでもあるので報告すべきかとても迷ったのですが、やっぱりしておこうと思いました。

    2009年10月28日 12:38