2015年7月6日月曜日

C#でLEFT OUTER JOIN (左外部結合)

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すればいいんじゃないかな?多分。

0 件のコメント:

コメントを投稿