複数のデータテーブルやコレクションのデータを並べ替えたり、絞り込みたいです。

タグの編集
投稿者 メタルスライム  (社会人) 投稿日時 2017/9/13 09:04:08
VB初心者です。
環境はVS2015&SQLSERVER2014です。

やりたい事は、
一旦、SQLSERVERからSQLで取得したデータをデータテーブルに保持し
そのデータを並べ替えて表示したり、絞り込んだりしたい訳です。

私が調べた限りは
・データテーブルは、SELECTというメソッドで、条件を指定して、絞り込む事が可能。
・コレクションの方が、LINQが使えて、さらに便利(LINQ自体、あまり詳しくありませんが。。。)
ということは分かってきました。

コレクションの方が、where条件等、SQL的な事ができそうなので、
データテーブルをLIST等のコレクションにコンバートしてから、条件による並べ替えや絞り込みを行おうと考えています。

ただ今悩んでいるのは、
①動的な複数の検索条件による絞り込み
②2つ以上のコレクションのJOIN
です。

例えば、LIST_AとLIST_Bというリストで、
Dim Query = From A In LIST_A
              Join B In LIST_B On A.ID Equals B.ID
とJOINします。

①LIST_Bの項目1のAとBで絞り込みたいと画面から指定があった場合
Where B.項目=A Or B項目=B
となると思いますが、条件がA,B,C,D....と続く場合、動的にどう実装すれば良いか
悩んでいます。

②絞り込みや並べ替えに必要な情報が足りないので、
例えばLIST_BのNAMEという項目に紐づけようと、LIST_CをJOINしようと
Join C In LIST_C On B.NAME Equals C.NAME
としたのですが、上手く結合した結果が返却されませんでした。
(LIST_AとLIST_Bだけの場合、上手くいきました)

申し訳ありませんが、皆様のお知恵を拝借したく存じます。
宜しくお願い致します。


             
投稿者 (削除されました)  () 投稿日時 2017/9/13 10:55:25
(削除されました)
投稿者 魔界の仮面弁士  (社会人) 投稿日時 2017/9/13 12:03:40
> LINQ自体、あまり詳しくありませんが。。。

LINQ には、SQL チックな記述が魅力的な「クエリ構文」と
 Dim q = From entry In collection Where …
より強力な「メソッド構文」による形式の 2 種類があります。
 Dim q = collection.Where(Function(entry) …)


LINQ の機能によっては、メソッド構文でないと呼び出せないものもあります。
(一方、クエリ構文で呼び出せる機能は、すべて同等のメソッド構文が存在します)


> データテーブルをLIST等のコレクションにコンバートしてから、条件による並べ替えや絞り込みを行おうと考えています。

その必要はありません。

DataTable.Rows は DataRow のコレクションなので、LINQ でも処理できますよ。
それに DataView も、DataRowView のコレクションですしね。

http://msdn.microsoft.com/ja-jp/library/bb386977.aspx
http://msdn.microsoft.com/ja-jp/library/bb386921.aspx


また、DataTable を LINQ で抽出した場合は、
 Dim resultTable As DataTable = q.CopyToDataTable()
のように、抽出結果を DataTable として受け取ることもできます。



> ①動的な複数の検索条件による絞り込み

「複数の検索条件」というのが、SQL で言うところの
 WHERE TBL.ID IN ('01', '03', '06', '10', '15')
のような物だとしたら、
 Dim data As String() = {"01", "03", "06", "10", "15"}
 Dim q = From r In dataTable1 Where data.Contains(r.Field(Of String)("ID"))
のように、Contains 拡張メソッドを使うことができます。


「複数の検索条件」というのが、SQL で言うところの
 WHERE TBL.COL1 = 'A'
  AND TBL.COL2 = 'B'
  AND TBL.COL3 = 'C'
のようなものだとしたら、
 Dim p1 = "A", p2 = "B", p3 = "C"
 Dim q = dataTable1.AsEnumerable()
 q = q.Where(Function(r) r("COL1") = p1)
 q = q.Where(Function(r) r("COL2") = p2)
 q = q.Where(Function(r) r("COL3") = p3)
のように、パイプラインを連ねることで記述できます。



しかし「複数の検索条件」というのが、SQL で言うところの
 WHERE TBL.ID = '01'
  OR TBL.ID = '02'
  OR TBL.COL1 = 'A'
  OR TBL.COL2 = 'B'
  OR TBL.COL3 = 'C'
のようなものになると、これは少々厄介です。


(案1) OR 条件を反転させて AND 条件に変換する。(ド・モルガンの法則)
 'Not And な条件で抽出
 Dim qNotAnd = dataTable1.AsEnumerable()
 qNotAnd = qNotAnd.Where(Function(r) r("ID") <> "01")
 qNotAnd = qNotAnd.Where(Function(r) r("ID") <> "02")
 qNotAnd = qNotAnd.Where(Function(r) r("COL1") <> "A")
 qNotAnd = qNotAnd.Where(Function(r) r("COL2") <> "B")
 qNotAnd = qNotAnd.Where(Function(r) r("COL3") <> "C")
 '最後にそれを元のコレクションから取り除く
 Dim q = dataTable1.AsEnumerable().Except(qNotAnd)


(案2) ラムダ式による OR 条件のリストを Any に渡す。
 '検索条件のコレクションを用意しておく
 Dim OrConditions As New List(Of Func(Of DataRow, Boolean))()
 OrConditions.Add(Function(r) r("ID") = "01")
 OrConditions.Add(Function(r) r("ID") = "02")
 OrConditions.Add(Function(r) r("COL1") = "A")
 OrConditions.Add(Function(r) r("COL2") = "B")
 OrConditions.Add(Function(r) r("COL3") = "C")
 'いずれかの条件を満たすものを抽出
 Dim q = From r In dataTable1 Where OrConditions.Any(Function(m) m(r))


(案3) System.Linq.Expressions.Expression を使う。
 長くなりそうなのでサンプルは省略。
 強力な機能ではあるのですが、式ツリーの構築に手間がかかります。


> ②絞り込みや並べ替えに必要な情報が足りないので、
> 例えばLIST_BのNAMEという項目に紐づけようと、LIST_CをJOINしようと
> Join C In LIST_C On B.NAME Equals C.NAME
> としたのですが、上手く結合した結果が返却されませんでした。

「必要な情報が足りない」というのは、どういう意味でしょうか?

それぞれの LIST がどういう内容になっていて、
それをどのように結合したいのか、具体的に書いてもらえれば、
サンプルを提示できるかもしれません。 
投稿者 shu  (社会人) 投稿日時 2017/9/13 12:54:09
Dim q~まではサンプルデータを作っているだけなので
それ以降が該当処理になってきます。
希望の処理を実現する箇所は

Where Function()
End Function()

のところになってきます。
ここに条件処理を記述すれば動的な判定が出来ます。
長くなってしまうようであれば

Where Function() Check(a,b)

としてCheckを外部に定義する方法もあります。ただしこの場合a,bの
型がCheckで認識できないといけないので作成済のクラスのインスタンスとして
渡す必要があります。


        Dim tbl1 As New DataTable()
        Dim tbl2 As New DataTable()

        With tbl1.Columns
            .Add("ID"GetType(Integer))
            .Add("Name"GetType(String))
            .Add("CID"GetType(Integer))
        End With

        With tbl2.Columns
            .Add("ID"GetType(Integer))
            .Add("Name"GetType(String))
        End With

        tbl1.Rows.Add(1, "Name1", 1)
        tbl1.Rows.Add(2, "Name2", 2)
        tbl1.Rows.Add(3, "Name3", 1)
        tbl1.Rows.Add(4, "Name4", 2)
        tbl1.AcceptChanges()
        tbl2.Rows.Add(1, "CName1")
        tbl2.Rows.Add(2, "CName2")
        tbl2.AcceptChanges()

        Dim q1 = (From r1 In tbl1.AsEnumerable
                  Select id = CInt(r1("ID")), Name = CStr(r1("name")), CID = CInt(r1("cid")))

        Dim q2 = (From r2 In tbl2.AsEnumerable
                  Select id = CInt(r2("ID")), CName = CStr(r2("name")))

        Dim q = (From a In q1
                 Join b In q2 On a.CID Equals b.id
                 Where Function()
                           Dim ret = a.id < 3
                           ret = ret AndAlso b.id = 1
                           Return ret
                       End Function())

        For Each itm In q
            Console.WriteLine($"{itm.a.id} {itm.a.Name} {itm.b.CName}")
        Next

投稿者 メタルスライム  (社会人) 投稿日時 2017/9/14 23:13:09
魔界の仮面弁士さん
ご回答、ありがとうございました。
とても勉強になりました。

まず、②のJOINの問題は、自己解決しました。
お騒がせしてすみません。

①の動的な複数の検索条件による絞り込み
についてですが、ご回答で
>しかし「複数の検索条件」というのが、SQL で言うところの
> WHERE TBL.ID = '01'
>  OR TBL.ID = '02'
>  OR TBL.COL1 = 'A'
>  OR TBL.COL2 = 'B'
>  OR TBL.COL3 = 'C'
>のようなものになると、これは少々厄介です。

と書かれたいた内容そのもので、やはり厄介なのですね。。。

>(案3) System.Linq.Expressions.Expression を使う。
自分でも、多少調べてみましたが、今やりたいことを実装しようと思うと
式ツリーしかなさそうに思います(涙)

shuさん
サンプルコード、ありがとうございました。
ですが、私のスキル不足で、解決には至りませんでした。


①動的な複数の検索条件による絞り込み
について、条件について情報を以下に追記いたします。
(検索画面を張りたかったのですが、できなさそうでしたので、文字情報のみになります)

以下のような、検索条件の処理について、LINQを含め、実装方法について、ご教示いただきたく存じます。

★検索条件★
・項目1:性別(男性、女性)→ラジオボタンで、どちらか選択
・項目2:血液型(A、B、O、AB)→チェックボックスで、複数選択可
・項目3:身長(cm)→テキストボックス入力で範囲指定
・項目4:体重(kg)→テキストボックス入力で範囲指定
・項目5:条件(条件1、2、3・・・)→チェックリストボックスで、複数選択可

※項目5は他のテーブルから取得して、条件を表示する為、条件数が動的に可変。

★検索条件例★
性別が男性、血液型がA or AB、身長が160~180cm、体重が50~90kg、条件は条件1、3に該当するデータ

SQLだと
where 
性別=男性 and 
血液型 in (A,AB) and
身長 Between 160 and 180 and
体重 Between 50 and 90 and
条件 in (条件1,条件3)
のような記述になると思います。


 
投稿者 shu  (社会人) 投稿日時 2017/9/15 08:27:10
> SQLだと
> where 
> 性別=男性 and 
> 血液型 in (A,AB) and
> 身長 Between 160 and 180 and
> 体重 Between 50 and 90 and
> 条件 in (条件1,条件3)
> のような記述になると思います。
先の私の提示コードにおいて

Where Fucntion ()
End Function()
の間を
Dim ret = True
ret = ret AndAlso A.性別="男性"
ret = ret AndAlso (A.血液型="A" OrElse A.血液型="AB")
ret = ret AndAlso (A.身長 >= 160 AndAlso A.身長 <= 180)
ret = ret AndAlso (A.体重 >= 50 AndAlso A.体重 <= 90)
ret = ret AndAlso (B.条件 = 条件1 OrElse B.条件 = 条件3)
Return Ret

のような感じにするとよいです。
投稿者 shu  (社会人) 投稿日時 2017/9/15 08:50:47
項目5の条件についてはさらに

If Ret Then
    Ret = False
    For i =0 to 9
        if 条件(idx) Then
            Ret = True
            Exit For
        End If
    Next
End If
のようにループにしたりすることも出来ますし
関数内の処理になるので色々な条件判断にも
対応できるかと思います。
投稿者 メタルスライム  (社会人) 投稿日時 2017/9/17 23:42:39
shuさん

ご回答いただきありがとうございます。
サンプルの内容については、
ご教示いただいた
Where Fucntion ()
End Function()
で対応出来ました。

>項目5の条件についてはさらに
>
>If Ret Then
>    Ret = False
>    For i =0 to 9
>        if 条件(idx) Then
>            Ret = True
>            Exit For
>        End If
>    Next
>End If
>のようにループにしたりすることも出来ますし
>関数内の処理になるので色々な条件判断にも
>対応できるかと思います。 

項目5は実際、ループの件数が可変ですので、
件数-1のFor~Nextか、ForEach~Nextで対応してみます。

実際、フィルタ条件は20項目近くあり、
その何項目かは、別のマスタから一旦データテーブルに落とし込んで、そのデータテーブルと
JOINして、絞り込む
というような処理になりそうですので、かなりややこしいです。。。

ただ
Where Fucntion ()
End Function()
は、かなり応用が利きそうに思いますので、上手く条件を整理すれば
コードは書けそうな気がしますので、この件は解決済とさせていただきます。

色々とご教示ありがとうございました。