ページ

2011年4月11日月曜日

◆LINQでの外部結合処理

通常Joinには内部結合と外部結合がある。
内部結合は既にJoinの所でやったので今度は外部結合である。

リレーションが張ってあるテーブルではちょっとやりずらいのでいつもお世話になっている@ITのサイトの例を参考にさせてもらった。
第7回 LINQ応用編 - @IT

Joinの所でも思ったのだが、どうもこの自分でテーブルを結合させるパターンの拡張メソッド方式は判りづらい。というか文法的に相性が悪いように思う。
はっきり言って素のSQLよりはるかに複雑になってしまう。
これでは本末転倒というものだ。

そこで今回は先に埋め込みクエリー方式でやってみた。(というか上記サンプルが埋め込みクエリー方式で書いてあるので)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication3
{
class Program
{
class 商品情報
{
public int Id;
public string 名前;
}

class 商品販売価格
{
public int Id;
public string 店名;
}

static void Main(string[] args)
{
商品情報[] 商品情報データ =
{
new 商品情報() { Id=1, 名前="PC-8001" },
new 商品情報() { Id=2, 名前="MZ-80K" },
new 商品情報() { Id=3, 名前="Basic Master Level-3" },
new 商品情報() { Id=4, 名前="COMKIT 8060" },
};

商品販売価格[] 商品販売価格データ =
{
new 商品販売価格() { Id=1, 店名="BitOut" },
new 商品販売価格() { Id=1, 店名="富士山音響" },
new 商品販売価格() { Id=2, 店名="富士山音響" },
new 商品販売価格() { Id=3, 店名="マイコンセンターROM" },
new 商品販売価格() { Id=3, 店名="富士山音響" },
};

//埋め込みクエリー方式での外部結合
var query = from x in 商品情報データ
join y in 商品販売価格データ on x.Id equals y.Id into z
from a in z.DefaultIfEmpty(
new 商品販売価格() { 店名 = "取り扱い店なし" })
select new { Name = x.名前, 店名 = a.店名 };

foreach (var 商品 in query)
{
Console.WriteLine("{0}", 商品.Name);
Console.WriteLine("\t{0}", 商品.店名);
}

Console.WriteLine("----------------------------------------");

//埋め込みクエリー方式での外部結合2
var query2 = from x in 商品情報データ
join y in 商品販売価格データ on x.Id equals y.Id into z
from a in z.DefaultIfEmpty()
select new { Name = x.名前, 店名 = a == null ? "取り扱い店なし" : a.店名 };

foreach (var 商品 in query2)
{
Console.WriteLine("{0}", 商品.Name);
Console.WriteLine("\t{0}", 商品.店名);
}

Console.WriteLine("----------------------------------------");
//拡張メソッド方式での外部結合
var query3 = 商品情報データ.GroupJoin(商品販売価格データ,
x => x.Id,
y => y.Id,
(x, g) => new
{
t1 = x,
t2 = g
})
.SelectMany(t => t.t2.DefaultIfEmpty(
new 商品販売価格() { 店名 = "取り扱い店なし" }),
(t, a) => new
{
Name = t.t1.名前,
店名 = a.店名
});
foreach (var 商品 in query3)
{
Console.WriteLine("{0}", 商品.Name);
Console.WriteLine("\t{0}", 商品.店名);
}
}
}
}
商品情報クラスと商品販売価格クラスを1対多の関係で使っている。

埋め込みクエリー方式では商品情報と商品販売価格をJoinしてIDが一致した商品販売価格のコレクションをzに入れている。
その後、商品販売コレクションのzを再度Fromに指定して商品情報と商品販売価格の内容をSelectしている。(このタイミングというかコンテキストで商品情報のxを参照できるのが、拡張メソッド方式に比べて埋め込みクエリー方式を簡単にしている理由ではなかろうか)


ここでこれまで出てきていないDefaultIfEmpty拡張メソッドなるものが登場している。
これは、外部結合(ここでは左外部結合)では、一致する右側のデータが存在しない場合がある。
その時に表示させるDefault値を指定するのがDefaultIfEmptyだ。
この例ではzが商品販売価格のコレクションなので、商品販売価格のオブジェクトをnewで作って指定している。


2つ目の埋め込みクエリー方式の例ではこの部分でデフォルトオブジェクトを指定せずにSelectの段階で商品販売価格がnullかどうかを判定し、デフォルト値を指定している。
それならば、DefaultInEmptyは必要ないのでは?という感じを抱くかもしれないが、これがないと外部結合にならない。(Id=4, 名前="COMKIT 8060"のデータが出力されない)
DefaultIfEmptyがない場合はnullではなく中身が空のオブジェクトになるようだ。


3つめが拡張メソッド方式。
73行目~77行目で商品情報クラス(x)に商品販売クラスのコレクション(g)を結合させたオブジェクトを作っている。
SelectManyでxとyの全パターンの組み合わせを作り出した上で新たな匿名クラスを作り出している。
判りづらいので図を使って説明すると。(図示し辛いので改行位置を変えた)
image


SelectManyの役割はパイプライン入力である赤で囲った匿名クラスのコレクションと、青で囲った部分のコレクションの組み合わせを作ることである。
赤がLEFT OUTER JOINの左側、青が右側。
そしてそれぞれの子要素を(t,a)として参照している。(tが赤の子要素,aが青の子要素)
青で囲んだ部分は極端な話、何も処理せずにtとは無関係なコレクションを指定してもよいのだが、ここでは入力パイプラインの子要素に対する前処理が可能になっているのでDefaultIfEmptyを使ってデフォルト値を補っている。


参照の変遷を書くとこんな感じだろうか。
image


何度も変数名が変わり非常に煩雑なのが判る。
素人考えだがLeftOuterJoinメソッドを作ってこんな感じにはならないものだろうか。


var query = 商品情報データ.LeftOuterJoin(商品販売価格データ,
    x => x.Id,
    y => y.Id,
    g => g.DefaultIfEmpty(new 商品販売価格(){店名 = “無し”}),
    a = > new{Name = x.名前,店名 = a.店名);

0 件のコメント:

コメントを投稿

私が最近チェックした記事