Entity Framework Core でテーブルのGROUP JOIN (テーブル結合)をする - C#

Entity Framework Core でテーブルのGROUP JOIN (テーブル結合)をするコードを紹介します。

概要

Entity Framework Core ではオブジェクト階層の構造でレコードを結合するGroupJoinの機能があります。 一般的なリレーショナルデータベースには無い機能のため、対応する SQL文はありません。
この記事では、GroupJoinを利用するとどのような動作になるか、どのようなデータ構造が取得できるかを紹介します。

書式

メソッド形式

[結合元DbSetオブジェクト].GroupJoin([結合先DbSetオブジェクト], [結合元レコード一時名] => [結合元レコード結合フィールド],
  [結合先レコード一時名] => [結合元レコード結合フィールド], 
  ([結合元レコード一時名], [結合先レコード一時名]) => new { [結合元レコード一時名], [結合先レコード一時名] });

クエリ形式

from [結合元レコード一時名] in [結合元DbSetオブジェクト] 
  join [結合先レコード一時名] in [結合先DbSetオブジェクト] on [結合元レコード結合フィールド] equals [結合先レコード結合フィールド]
  into [結果レコード一時名] select new { [結合元レコード一時名], [結果レコード一時名] };

プログラム例:メソッド形式

テーブル

以下のテーブルを作成します。
hst_level1
id label value
1 sound 音楽
2 image 画像
3 movie 動画
hst_level2
id parent label value
1 1 wav wave-sound
2 1 mp3 mp3-sound
3 1 flac flac-sound
4 2 jpeg jpeg-image
5 2 png png-image
6 3 mp4 mp4-movie
7 3 wmv wmv-movie

UI

下図のフォームを作成します。ボタンと複数行のテキストボックスを配置します。

コード

以下のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using EntityFrameworkCoreJoin.Models;

namespace EntityFrameworkCoreJoin
{
  public partial class FormGroupJoin : Form
  {
    public FormGroupJoin()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
      IQueryable<dynamic> rec;
      IPentecSandBoxContext sc = new IPentecSandBoxContext();

      rec = sc.HstLevel1s.GroupJoin(sc.HstLevel2s, hst1 => hst1.Id, hst2 => hst2.Parent, (hst1, hst2) => new { hst1, hst2 });

      foreach (var r in rec) {
        foreach (var r2 in r.hst2) {
          textBox1.Text += string.Format("{0}:{1} - {2}:{3}\r\n", r.hst1.Label.Trim(), r.hst1.Value.Trim(), r2.Label.Trim(), r2.Value.Trim());
        }
      }
    }
  }
}

解説

HstLevel1 テーブルをHstLevel2とGroupJoinします。HstLevel1 テーブルのidフィールドと、HstLevel2テーブルのParentフィールドを結合します。 結果は、HstLevel1 テーブルのHstLevel1型とHstLevel2 テーブルのHstLevel2型の2つのオブジェクトを持つ匿名オブジェクトで返します。
  rec = sc.HstLevel1s.GroupJoin(sc.HstLevel2s, hst1 => hst1.Id, hst2 => hst2.Parent, (hst1, hst2) => new { hst1, hst2 });

foreachループで、結果の IQueryable<dynamic> recオブジェクトの内容をテキストボックスに表示します。
  foreach (var r in rec) {
    foreach (var r2 in r.hst2) {
      textBox1.Text += string.Format("{0}:{1} - {2}:{3}\r\n", r.hst1.Label.Trim(), r.hst1.Value.Trim(), r2.Label.Trim(), r2.Value.Trim());
    }
  }

今回のテーブルの場合、GroupJoinの結果は以下となります。
Join元のレコード1オブジェクトに対して結合先のレコードが配列状に格納されます。
rec rec.hst1.Label rec.hst1.Value rec.hst2[0].Label rec.hst2[0].Value rec.hst2[1].Label rec.hst2[1].Value rec.hst2[2].Label rec.hst2[2].Value
[0] sound 音楽 wav wave-sound mp3 mp3-sound flac flac-sound
[1] image 画像 jpeg jpeg-image png png-image
[2] movie 動画 mp4 mp4-movie wmv wmv-movie

オブジェクト内にオブジェクトの配列でJoin先のレコードの値が格納されるため、foreachのネスト文で値をテキストボックスに表示します。

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


[button1]をクリックします。テキストボックスに下図の結果が表示されます。


sound:音楽 - wav:wave-sound
sound:音楽 - mp3:mp3-sound
sound:音楽 - flac:flac-sound
image:画像 - jpeg:jpeg-image
image:画像 - png:png-image
movie:動画 - mp4:mp4-movie
movie:動画 - wmv:wmv-movie


サーバー側では以下のSQL文が実行されます。
SELECT [h].[id], [h].[label], [h].[value], [t].[id], [t].[label], [t].[parent], [t].[value]
FROM [hst_level1] AS [h]
OUTER APPLY (
    SELECT [h0].[id], [h0].[label], [h0].[parent], [h0].[value]
    FROM [hst_level2] AS [h0]
    WHERE [h].[id] = [h0].[parent]
) AS [t]
ORDER BY [h].[id]

プログラム例:クエリ形式

UI

下図のフォームを作成します。ボタンと複数行のテキストボックスを配置します。

コード

以下のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using EntityFrameworkCoreJoin.Models;

namespace EntityFrameworkCoreJoin
{
  public partial class FormGroupJoin : Form
  {
    public FormGroupJoin()
    {
      InitializeComponent();
    }

    private void button2_Click(object sender, EventArgs e)
    {
      IQueryable<dynamic> rec;
      IPentecSandBoxContext sc = new IPentecSandBoxContext();

      rec = from hst1 in sc.HstLevel1s join hst2 in sc.HstLevel2s on hst1.Id equals hst2.Parent into groupjoin select new { hst1, groupjoin };

      foreach (var r in rec) {
        foreach (var r2 in r.groupjoin) {
          textBox1.Text += string.Format("{0}:{1} - {2}:{3}\r\n", r.hst1.Label.Trim(), r.hst1.Value.Trim(), r2.Label.Trim(), r2.Value.Trim());
        }
      }

    }
  }
}

解説

先のコードと同じ処理です。LINQの記述をメソッド構文ではなく、クエリ構文で記述しています。クエリ構文で記述する場合にはjoinの記述の後ろにinto句を記述します。
  rec = from hst1 in sc.HstLevel1s join hst2 in sc.HstLevel2s on hst1.Id equals hst2.Parent into groupjoin select new { hst1, groupjoin };

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


[button2]をクリックします。先のコードと同じ結果が表示され、GroupJoinが実行できていることが確認できます。

プログラム例:匿名型を利用しないコード

UI

下図のフォームを作成します。

コード

以下のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using EntityFrameworkCoreJoin.Models;

namespace EntityFrameworkCoreJoin
{
  public partial class FormGroupJoin : Form
  {
    public class QueryResult
    {
      public HstLevel1 hst1;
      public IEnumerable<HstLevel2> hst2;
    }

    public FormGroupJoin()
    {
      InitializeComponent();
    }

    private void button3_Click(object sender, EventArgs e)
    {
      IQueryable<QueryResult> rec;
      IPentecSandBoxContext sc = new IPentecSandBoxContext();

      //メソッド構文
      rec = sc.HstLevel1s.GroupJoin(sc.HstLevel2s, h1 => h1.Id, h2 => h2.Parent, (h1, h2) => new QueryResult(){ hst1=h1, hst2=h2 });

      //クエリ構文
      //rec = from h1 in sc.HstLevel1s join h2 in sc.HstLevel2s on h1.Id equals h2.Parent into groupjoin select new QueryResult(){ hst1=h1, hst2=groupjoin };

      foreach (QueryResult r in rec) {
        foreach (HstLevel2 r2 in r.hst2) {
          textBox1.Text += string.Format("{0}:{1} - {2}:{3}\r\n", r.hst1.Label.Trim(), r.hst1.Value.Trim(), r2.Label.Trim(), r2.Value.Trim());
        }
      }
    }
  }
}

解説

結果を格納するクラスオブジェクトを宣言します。
1つ目のJOIN元のレコードは、テーブル型のメンバ変数を宣言します。 2つ目のJOIN先のレコードは複数のレコードが代入されるため、IEnumerable型で宣言します。
  public class QueryResult
  {
    public HstLevel1 hst1;
    public IEnumerable<HstLevel2> hst2;
  }

GroupJoinを実行し、結果をIQueryable<QueryResult>オブジェクトに代入します。'
  IQueryable<QueryResult> rec;
  rec = sc.HstLevel1s.GroupJoin(sc.HstLevel2s, h1 => h1.Id, h2 => h2.Parent, (h1, h2) => new QueryResult(){ hst1=h1, hst2=h2 });

クエリ構文の場合は、以下のコードになります。
  IQueryable<QueryResult> rec;
  rec = from h1 in sc.HstLevel1s join h2 in sc.HstLevel2s on h1.Id equals h2.Parent into groupjoin select new QueryResult(){ hst1=h1, hst2=groupjoin };

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。


[button3]をクリックします。先のコードと同じ結果が表示され、GroupJoinが実行できていることが確認できます。

補足:Join先がない場合の動作

hst_level1のテーブルを以下に変更します。
hst_level1
id label value
1 sound 音楽
2 image 画像
3 movie 動画
4 document 文章

以下のコードに変更します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using EntityFrameworkCoreJoin.Models;

namespace EntityFrameworkCoreJoin
{
  public partial class FormGroupJoin : Form
  {
    public class QueryResult
    {
      public HstLevel1 hst1;
      public IEnumerable<HstLevel2> hst2;
    }

    public FormGroupJoin()
    {
      InitializeComponent();
    }

    private void button3_Click(object sender, EventArgs e)
    {
      IQueryable<QueryResult> rec;
      IPentecSandBoxContext sc = new IPentecSandBoxContext();

      rec = sc.HstLevel1s.GroupJoin(sc.HstLevel2s, h1 => h1.Id, h2 => h2.Parent, (h1, h2) => new QueryResult(){ hst1=h1, hst2=h2 });
      //rec = from h1 in sc.HstLevel1s join h2 in sc.HstLevel2s on h1.Id equals h2.Parent into groupjoin select new QueryResult(){ hst1=h1, hst2=groupjoin };

      foreach (QueryResult r in rec) {
        textBox1.Text += string.Format("{0}:{1}\r\n", r.hst1.Label.Trim(), r.hst1.Value.Trim());
        foreach (HstLevel2 r2 in r.hst2) {
          textBox1.Text += string.Format("{0}:{1} - {2}:{3}\r\n", r.hst1.Label.Trim(), r.hst1.Value.Trim(), r2.Label.Trim(), r2.Value.Trim());
        }
      }
    }
  }
}

プロジェクトを実行し[button3]をクリックすると下図の結果が表示されます。
Join先がないレコードもrec変数には代入されます。ただしJoin先が無いため、hst2の長さは0になり、何も代入されていない状態です。

著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
最終更新日: 2023-07-09
作成日: 2023-07-08
iPentec all rights reserverd.