การใช้ Join ใน LINQ (C#)
Join ใช้รวมข้อมูลจาก 2 แหล่ง (Collection/Table) ตามเงื่อนไขที่กำหนด คล้าย SQL JOIN
ประเภท Join
- Inner Join - คืนเฉพาะรายการที่มีคู่ในทั้งสองฝั่ง (Default ของ LINQ Join)
- Left Join - คืนทุกฝั่งซ้าย + ข้อมูลฝั่งขวาที่มีคู่ (ใช้ GroupJoin + DefaultIfEmpty)
- Cross Join - Cartesian Product (ใช้ SelectMany)
Syntax: Method Syntax (Lambda) vs Query Syntax
Method Syntax (Lambda)
var result = employees.Join(departments,
e => e.DepartmentID, // Outer key selector
d => d.ID, // Inner key selector
(e, d) => new // Result selector
{
EmployeeName = e.Name,
DepartmentName = d.Name
});
Query Syntax (คล้าย SQL)
var result = from e in employees
join d in departments on e.DepartmentID equals d.ID
select new
{
EmployeeName = e.Name,
DepartmentName = d.Name
};
ตัวอย่างครบ: Inner Join พร้อม Class Definition
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
// Method Syntax (Lambda)
var resultLambda = Employee.GetAllEmployees()
.Join(Department.GetAllDepartments(),
e => e.DepartmentID,
d => d.ID,
(e, d) => new
{
EmployeeName = e.Name,
DepartmentName = d.Name
});
Console.WriteLine("=== Lambda Syntax ===");
foreach (var emp in resultLambda)
{
Console.WriteLine($"{emp.EmployeeName}\t{emp.DepartmentName}");
}
// Query Syntax
var resultQuery = from e in Employee.GetAllEmployees()
join d in Department.GetAllDepartments()
on e.DepartmentID equals d.ID
select new
{
EmployeeName = e.Name,
DepartmentName = d.Name
};
Console.WriteLine("\n=== Query Syntax ===");
foreach (var emp in resultQuery)
{
Console.WriteLine($"{emp.EmployeeName}\t{emp.DepartmentName}");
}
}
}
public class Employee
{
public int ID { get; set; }
public string Name { get; set; }
public int DepartmentID { get; set; }
public static List GetAllEmployees()
{
return new List
{
new Employee { ID = 1, Name = "A", DepartmentID = 1 },
new Employee { ID = 2, Name = "B", DepartmentID = 2 },
new Employee { ID = 3, Name = "B", DepartmentID = 1 },
new Employee { ID = 4, Name = "V", DepartmentID = 1 },
new Employee { ID = 5, Name = "F", DepartmentID = 2 },
new Employee { ID = 6, Name = "R", DepartmentID = 2 },
new Employee { ID = 7, Name = "TT", DepartmentID = 1 },
new Employee { ID = 8, Name = "YY", DepartmentID = 1 },
new Employee { ID = 9, Name = "WW", DepartmentID = 2 },
new Employee { ID = 10, Name = "QQ" } // ไม่มี DepartmentID
};
}
}
public class Department
{
public int ID { get; set; }
public string Name { get; set; }
public static List GetAllDepartments()
{
return new List
{
new Department { ID = 1, Name = "IT" },
new Department { ID = 2, Name = "HR" },
new Department { ID = 3, Name = "Contract" } // ไม่มีพนักงาน
};
}
}
ผลลัพธ์:
=== Lambda Syntax === A IT B HR B IT V IT F HR R HR TT IT YY IT WW HR === Query Syntax === A IT B HR B IT V IT F HR R HR TT IT YY IT WW HR
หมายเหตุ: พนักงาน "QQ" (ID=10) ไม่ปรากฏ เพราะไม่มี DepartmentID ตรงกับ Department ใดๆ (Inner Join) และ Department "Contract" (ID=3) ก็ไม่ปรากฏ เพราะไม่มีพนักงาน
Left Join (รองรับฝั่งซ้ายที่ไม่มีคู่)
// Left Join: พนักงานทุกคน แม้ไม่มี Department
var leftJoin = from e in Employee.GetAllEmployees()
join d in Department.GetAllDepartments()
on e.DepartmentID equals d.ID into deptGroup
from d in deptGroup.DefaultIfEmpty() // Key point!
select new
{
EmployeeName = e.Name,
DepartmentName = d?.Name ?? "No Department"
};
// ผลลัพธ์จะมี QQ - No Department
Join หลายเงื่อนไข (Composite Key)
// ใช้ Anonymous Type สำหรับ Key หลายคอลัมน์
var result = orders.Join(orderDetails,
o => new { o.OrderID, o.CustomerID }, // Composite key
d => new { d.OrderID, d.CustomerID },
(o, d) => new { o.OrderID, o.CustomerID, d.ProductID, d.Quantity });
GroupJoin (Hierarchical Data)
// Department พร้อมรายชื่อพนักงาน (1-N)
var deptWithEmps = departments.GroupJoin(employees,
d => d.ID,
e => e.DepartmentID,
(d, emps) => new
{
DepartmentName = d.Name,
Employees = emps.Select(e => e.Name).ToList()
});
foreach (var dept in deptWithEmps)
{
Console.WriteLine($"Department: {dept.DepartmentName}");
foreach (var emp in dept.Employees)
{
Console.WriteLine($" - {emp}");
}
}
Performance Tips
- Key Selector ควรเป็น Primitive Type (int, string, Guid) เร็วกว่า Object
- Index/Hash Join - LINQ to Objects ใช้ Hash Join อัตโนมัติ (O(n+m))
- LINQ to SQL/EF Core - แปลงเป็น SQL JOIN จริงใน Database
- AsParallel() - สำหรับข้อมูลเยอะมาก (PLINQ)
สรุป
- Inner Join:
Join()หรือjoin ... on ... equals - Left Join:
GroupJoin() + DefaultIfEmpty()หรือjoin ... into ... from ... DefaultIfEmpty() - Composite Key: ใช้ Anonymous Type
new { Prop1, Prop2 } - Query Syntax อ่านง่ายสำหรับ Join 複雜, Lambda ยืดหยุ่นกว่าสำหรับ Chaining