LINQを使って、「LEFT OUTER JOIN」をしたかった。普通のJoinだとINNER JOINなので、少し工夫する必要がありそう。ググってみると、以下のようなコードが一般的なようだ。
var query = from person in people
join pet in pets on person equals pet.Owner into gj
from subpet in gj.DefaultIfEmpty()
select new
{
person.FirstName,
PetName = (subpet == null ? String.Empty : subpet.Name)
};
ちなみにこれは、
MSDNの「方法 : 左外部結合を実行する (C# プログラミング ガイド)」から。
大体どこを見てもこんな感じで、ほとんど左外部結合をしたいときの慣用句(イディオム)みたいなもののよう。とはいえ、このコードを見て「あ、左外部結合させたいんだな」と思える人がどれくらいいるか。要は、コードから意図が解りづらいので、あまり好みじゃないなぁ。と。
もう少し、コードを見て左外部結合であることが解るように、拡張メソッドを作ってみた。こんなの。
public static class EnumerableEx
{
public static IEnumerable<TResult> OuterJoin<TOuter, TInner, TKey, TResult>
(this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter, TInner, TResult> resultSelector,
TInner innerDefaultValue)
{
return outer
.GroupJoin(
inner,
outerKeySelector,
innerKeySelector,
(o, i) => new { Out = o, Ins = i.DefaultIfEmpty(innerDefaultValue) })
.SelectMany(g => g.Ins, (g, i) => resultSelector(g.Out, i));
}
}
Enumerable.Joinメソッドと比べると、最後のパラメータが余計についてます。これはOuterの要素に対して合致するInnerの要素がなかった時の代替要素を指定するもの。Null Objectだと思っておけばよいと思います。
あと、名前は長いのを嫌い、「OuterJoin」としています。タイプパラメータからも、左が外なのは明らかなので、特に問題はないと思っています。
ちなみに、これをライブラリ的に用意するなら、GroupJoinするときにEqautityComparerを指定するパターン、それと、DefaultIfEmptyでTInnerのデフォルト値を自動的に使うパターン、その組み合わせで4つのオーバーロードを、用意しておくのがよいと思います。
さて、テストします。
static void Main(string[] args)
{
var products = new[]
{
new { ProductId = 1, Name = "えんぴつ" },
new { ProductId = 2, Name = "けしごむ" },
new { ProductId = 3, Name = "コンパス" },
new { ProductId = 4, Name = "クレヨン" },
};
var sales = new[]
{
new { SaleId = 1, ProductId = 1, Buyer = "○○商会", Quantity = 2 },
new { SaleId = 2, ProductId = 1, Buyer = "××文具店", Quantity = 4 },
new { SaleId = 3, ProductId = 2, Buyer = "△屋", Quantity = 3 },
new { SaleId = 4, ProductId = 4, Buyer = "○○商会", Quantity = 1 },
new { SaleId = 5, ProductId = 5, Buyer = "○○商会", Quantity = 1 },
};
var nosaled
= new { SaleId = 0, ProductId = 0, Buyer = "(no sales)", Quantity = 0 };
var salesInfo = products.OuterJoin(
sales,
p => p.ProductId,
s => s.ProductId,
(p, s) => new { p.Name, s.Buyer, s.Quantity },
nosaled);
foreach (var i in salesInfo)
{
Console.WriteLine("{0}, {1}, {2}", i.Name, i.Buyer, i.Quantity);
}
}
結果はこう。
えんぴつ, ○○商会, 2
えんぴつ, ××文具店, 4
けしごむ, △屋, 3
コンパス, (no sales), 0
クレヨン, ○○商会, 1
…右外部結合?RIGHT OUTER JOINか。僕自身使ったことないですが、必要なら右左を入れ替えてあげればいいはず。上のコードの20行目から28行目を、こんな感じに置き換えてみる。
var noproduct = new { ProductId = 0, Name = "(no item)" };
var salesInfo = sales.OuterJoin(
products,
s => s.ProductId,
p => p.ProductId,
(s, p) => new { p.Name, s.Buyer, s.Quantity },
noproduct);
結果はこう。
えんぴつ, ○○商会, 2
えんぴつ, ××文具店, 4
けしごむ, △屋, 3
クレヨン, ○○商会, 1
(no item), ○○商会, 1
うん。いんじゃないかな?
…完全外部結合?…FULL OUTER JOIN…。必要?それ。
使い道が解らなくてモチベーションゼロだけど、要するに左外部結合+右のみに存在するレコードを、編集してUnionすればいいんじゃないかな?多分。