今回から数回に分けて、結合について記述します。
結合とは、文字通り特定のシーケンスと特定のシーケンスをキー単位で等しいもの
同士をくっつける処理です。
Linqでは、以下の結合処理が用意されています。
- 内部結合
- グループ化結合
- 左外部結合
今回は、内部結合についてです。
利用するキーワードはjoinです。
構文は以下のようになります。
from x in ListA join y in ListB on x.Id equals y.Id
結合では2つのシーケンスが必要となりまして
上記の例でいうとListAを外側のシーケンス、ListBを内側のシーケンスと
いいます。onの右に結合条件を記述します。
つまり以下のようになります。
from x in 外側のシーケンス join y in 内側のシーケンス on 外側のシーケンスのキー equals 内側のシーケンスのキー
結合条件の部分ですが、かならず上記の順番でないといけません。
また、Linqは等結合しかサポートされていません。
常に、一致する条件を指定する必要があります。
後、これ結構重要な事なのですが、joinキーワード内での内側のシーケンスは
join内で完結させていないといけません。日本語にするのが難しいのですが
from x in ListX join y in ListY on x.Id equals y.Id
はオッケイですが、
from x in ListX from y in ListY join z in y.ListZ on x.Id equals z.Id
は、yという名前はコンテキストに存在しないというエラーになります。
joinはgroup byと同じく新たなコンテキストを発生させるので
こうなる模様です。
この制約は、1対1でオブジェクトが紐づく場合は問題にならないのですが
以下のように一つのオブジェクト内に複数のIDを持っているオブジェクトが
N件ある場合などが問題なります。
class Person { public string Id { get; set; } ... } class Project { ... public IEnumerable<string> Members { get; set; } } IEnumerable<Person> persons = new []{ new Person(Id="0001"), new Person(Id="0002"), new Person(Id="0003"), ... }; IEnumerable<Person> projects = new []{ new Project(Members=new []{"0001", "0002", "0003"}), .... };
上記のような場合に
from project in projects from member in project.Members join person in persons on member equals person.Id
とするのは、エラーとなりません。内側のシーケンスである
personsはjoin内で収まっているからです。
でも、
from person in persons from project in projects join member in project.Member on person.Id equals member
はエラーとなります。
実際、このような場合は
from person in persons join project in ( from prj in projects from member in prj.Members select new { Id=..., Name=..., Member=member } ) on person.Id equals project.Member
のようにサブクエリにしてやるとうまくいきます。
(私がLinq勉強中なのでやり方知らないだけかもしれません。いいやり方あったら教えて下さい。m(_ _)m)
で、以下サンプルです。
#region LinqSamples-09 public class LinqSamples09 : IExecutable { public class Person { public string Id { get; set; } public string Name { get; set; } public int Age { get; set; } public DateTime Birthday { get; set; } } public class Team { public string Id { get; set; } public string Name { get; set; } public IEnumerable<string> Members { get; set; } } public enum ProjectState { NotStarted ,Processing ,Done } public class Project { public string Id { get; set; } public string Name { get; set; } public IEnumerable<string> Members { get; set; } public DateTime From { get; set; } public DateTime? To { get; set; } public ProjectState State { get; set; } } // メンバー IEnumerable<Person> persons = new[]{ new Person { Id = "001" ,Name = "gsf_zero1" ,Age = 30 ,Birthday = new DateTime(1979, 11, 18) } ,new Person { Id = "002" ,Name = "gsf_zero2" ,Age = 20 ,Birthday = new DateTime(1989, 11, 18) } ,new Person { Id = "003" ,Name = "gsf_zero3" ,Age = 18 ,Birthday = new DateTime(1991, 11, 18) } ,new Person { Id = "004" ,Name = "gsf_zero4" ,Age = 40 ,Birthday = new DateTime(1969, 11, 18) } ,new Person { Id = "005" ,Name = "gsf_zero5" ,Age = 44 ,Birthday = new DateTime(1965, 11, 18) } }; // チーム IEnumerable<Team> teams = new[]{ new Team { Id = "001" ,Name = "team_1" ,Members = new []{ "001", "004" } } ,new Team { Id = "002" ,Name = "team_2" ,Members = new []{ "002", "003" } } }; // プロジェクト IEnumerable<Project> projects = new[]{ new Project { Id = "001" ,Name = "project_1" ,Members = new []{ "001", "002" } ,From = new DateTime(2009, 8, 1) ,To = new DateTime(2009, 10, 15) ,State = ProjectState.Done } ,new Project { Id = "002" ,Name = "project_2" ,Members = new []{ "003", "004" } ,From = new DateTime(2009, 9, 1) ,To = new DateTime(2009, 11, 25) ,State = ProjectState.Done } ,new Project { Id = "003" ,Name = "project_3" ,Members = new []{ "001" } ,From = new DateTime(2007, 1, 10) ,To = null ,State = ProjectState.Processing } ,new Project { Id = "004" ,Name = "project_4" ,Members = new []{ "001", "002", "003", "004" } ,From = new DateTime(2010, 1, 5) ,To = null ,State = ProjectState.NotStarted } }; public void Execute() { // // joinを用いた内部結合(INNER JOIN)のサンプル. // // joinキーワードの構文は以下の通り. // join 内部側の変数 in 内部側のシーケンス on 外部側のキー equals 内部側のキー // // Linqのjoinでは等結合(つまり等値の場合の結合)のみが行える。 // //==================================================================================== // Teamから辿るパターン var query1 = from team in teams from personId in team.Members join person in persons on personId equals person.Id orderby team.Id ascending, person.Id ascending select new { Team = team.Name ,Person = person.Name }; Console.WriteLine("========================================="); foreach (var item in query1) { Console.WriteLine(item); } // // Personから辿るパターン. // joinキーワードを利用せずに手動で結合処理. // // この場合、上記と同じように // // from person in persons // from team in teams // join personId in team.Members on person.Id equals personId // // とすると、コンテストが違うというエラーとなる。 // これは、join内部で新たなコンテキストが発生するからである。 // query1では、内部側のシーケンスをjoin内部だけで利用していたので // 問題ない. // var query2 = from person in persons from team in teams where team.Members.Contains(person.Id) orderby team.Id ascending, person.Id ascending select new { Team = team.Name ,Person = person.Name }; Console.WriteLine("========================================="); foreach (var item in query2) { Console.WriteLine(item); } // // query2をjoinで収めるパターン. // // 上記の例をjoinに収めるためには、join内で目的のシーケンスを // 構築する必要がある。そのため、以下の例では目的のシーケンスを作成するために // join内にて、さらにサブクエリを作成している。 // var query3 = from person in persons join team in ( from team in teams from member in team.Members select new { Id = team.Id ,Name = team.Name ,PersonId = member } ) on person.Id equals team.PersonId orderby team.Id ascending, person.Id ascending select new { Team = team.Name ,Person = person.Name }; Console.WriteLine("========================================="); foreach (var item in query3) { Console.WriteLine(item); } // // CROSS JOIN. // // 普通にfromを並べればCROSS JOIN状態となる。 // var query4 = from person in persons from team in teams orderby team.Id ascending, person.Id ascending select new { Team = team.Name ,Person = person.Name }; Console.WriteLine("========================================="); foreach (var item in query4) { Console.WriteLine(item); } } } #endregion
実行すると以下のようになります。
========================================= { Team = team_1, Person = gsf_zero1 } { Team = team_1, Person = gsf_zero4 } { Team = team_2, Person = gsf_zero2 } { Team = team_2, Person = gsf_zero3 } ========================================= { Team = team_1, Person = gsf_zero1 } { Team = team_1, Person = gsf_zero4 } { Team = team_2, Person = gsf_zero2 } { Team = team_2, Person = gsf_zero3 } ========================================= { Team = team_1, Person = gsf_zero1 } { Team = team_1, Person = gsf_zero4 } { Team = team_2, Person = gsf_zero2 } { Team = team_2, Person = gsf_zero3 } ========================================= { Team = team_1, Person = gsf_zero1 } { Team = team_1, Person = gsf_zero2 } { Team = team_1, Person = gsf_zero3 } { Team = team_1, Person = gsf_zero4 } { Team = team_1, Person = gsf_zero5 } { Team = team_2, Person = gsf_zero1 } { Team = team_2, Person = gsf_zero2 } { Team = team_2, Person = gsf_zero3 } { Team = team_2, Person = gsf_zero4 } { Team = team_2, Person = gsf_zero5 }