今回は、左外部結合です。
Linqでは外部結合もサポートされていますが、いわゆる左外部結合(LEFT OUTER JOIN)のみが行えます。
SQLでいうRIGHT OUTER JOINやFULL OUTER JOINはサポートされていません。
やり方なのですが、以下の手順を踏みます。
- まずグループ化結合を行います。
- グループ化結合を行ったものを再度fromキーワードでループ
- この際にDefaultIfEmptyメソッドを利用してデータが存在しないものを含めるようにします。
- 最後にselectキーワードで結果がDefault値のものを適切な値に変換します。
たとえば、以下のようなデータが存在するとします。
// メンバー 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<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 } };
上記のデータ上、PersonのIDが0005のデータはどのプロジェクトにも属していません。
この状態で、以下のクエリを発行すると0005のデータが結果に含まれません。
var query = from person in persons join prj in ( from project in projects from member in project.Members select new { Id = project.Id, Name = project.Name, Member = member } ) on person.Id equals prj.Member into personProjects from personProject in personProjects select new { Id = person.Id ,Name = person.Name ,Project = personProject.Name };
上記のクエリで0005のデータが含まれないのは、以下の部分にて最終的に結果から除外されているからです。
(PersonのIDが0005のデータは、グループ化結合を行ったタイミングで結果が0件となっています。)
from personProject in personProjects
これを以下のようにすることで、左外部結合状態となります。
from personProject in personProjects.DefaultIfEmpty()
以下が左外部結合版となります。
var query2 = from person in persons join prj in ( from project in projects from member in project.Members select new { Id = project.Id, Name = project.Name, Member = member } ) on person.Id equals prj.Member into personProjects // 外部結合するためにDefaultIfEmptyを使用. from personProject in personProjects.DefaultIfEmpty() select new { Id = person.Id ,Name = person.Name // 結合対象が存在しなかった場合、その型のdefault(T)の値となってので // 望みの形に変換する. ,Project = (personProject == null) ? "''" : personProject.Name };
注意点として、DefaultIfEmptyメソッドを利用するとデータが存在しない場合は、該当する型のdefault(T)の値が
返却されますので、最後のselectキーワードのタイミングで適切な値に変換する必要があります。
以下、サンプルです。
#region LinqSamples-11 public class LinqSamples11 : 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() { // // グループ化結合のサンプル // // グループ化結合したものにDefaultIfEmptyメソッドを適用したものが // Linqでの左外部結合となる。 // // // 外部結合していない版. // // 以下のクエリの場合、gsf_zero5が表示されない。 // (最後のfrom personProject in personProjectsの部分で除外される。) var query = from person in persons join prj in ( from project in projects from member in project.Members select new { Id = project.Id, Name = project.Name, Member = member } ) on person.Id equals prj.Member into personProjects from personProject in personProjects select new { Id = person.Id ,Name = person.Name ,Project = personProject.Name }; query.ToList().ForEach(Console.WriteLine); // // 外部結合版 // var query2 = from person in persons join prj in ( from project in projects from member in project.Members select new { Id = project.Id, Name = project.Name, Member = member } ) on person.Id equals prj.Member into personProjects // 外部結合するためにDefaultIfEmptyを使用. from personProject in personProjects.DefaultIfEmpty() select new { Id = person.Id ,Name = person.Name // 結合対象が存在しなかった場合、その型のdefault(T)の値となってので // 望みの形に変換する. ,Project = (personProject == null) ? "''" : personProject.Name }; Console.WriteLine("======================================================"); query2.ToList().ForEach(Console.WriteLine); } } #endregion
次からは、Linq to Objectの各メソッドをちょこちょこやっていこうと思います。