اپراتورهای joining در LINQ

02 اردیبهشت 1398
درسنامه درس 6 از سری آموزش LINQ
LINQ-Joining-operators

با سلام به کاربران عزیز با یکی دیگر از سری آموزش های LINQ در C# در خدمت شما هستیم. در این جلسه قصد داریم با اپراتور های joining آشنا شویم.

joining operators چیست؟

اگر با زبان SQL آشنایی داشته باشید می دانید که اپراتور Join برای پیوند دادن دو یا چند جدول بر اساس فیلدهایی مشخص و مشترک استفاده می شود. در LINQ نیز کاربرد آن دقیقا مانند زبان SQL است فقط روش اعمال آن متفاوت است که در ادامه با آن آشنا خواهید شد.

توجه: ذکر این نکته ضروری است که پیاده سازی اپراتور های joining با استفاده از کلمه ی Join است. فقط به کاربردن آن همراه با دیگر اپراتور ها باعث تغییر کارایی Join خواهد شد.

عملکرد joining در LINQ به چهار بخش:

  • INNER JOIN
  • LEFT OUTER JOIN
  • CROSS JOIN
  • GROUP JOIN

تقسیم بندی می شوند که در جدول زیر کاربرد هر یک بررسی شده است:

کاربرد نام اپراتور
عناصری را از مجموعه سمت چپ و راست بر اساس یک فیلد مشترک باز می گرداند INNER JOIN
 عناصر را از مجموعه سمت چپ و عناصر مربوطه از مجموعه سمت راست باز می گرداند LEFT OUTER JOIN
هر عنصر از مجموعه ی سمت چپ را به تمامی عناصر مجموعه ی سمت راست اضافه می کند (ربط می دهد) CROSS JOIN
عناصر را بر اساس مجموعه ی چپ و راست، به ترتیب گروه مرتب سازی می کند GROUP JOIN

پیاده سازی INNER JOIN

با استفاده از این متد می توان دو یا چند مجموعه را بر اساس فیلدهای مشخصی با هم ارتباط داد. توضیح این نوع اپراتورها شاید کمی گیج کننده به نظر برسد. اجازه دهید با بررسی یک مثال آن را توضیح دهیم:

مفهوم کلی Inner Join
مفهوم کلی Inner Join

الگوی استفاده از join به صورت زیر است:

var result = from d in objDept
join e in objEmp
on d.DepId equals e.DeptId
select new
{
EmployeeName = e.Name,
DepartmentName = d.DepName
};

دو شی objEmp و objDept نماینده ی دو مجموعه ی مختلف هستند که دارای فیلدهای مشخصی می باشند که یک فیلد بین دو مجموعه مشترک است در واقع خط ارتباطی این دو مجموعه (یا جدول) بر اساس فیلد مشترک است که در این الگو، فیلد مشترک DepId و DeptId است.

حالا مثال زیر را در  نظر بگیرید:

using System;
using System.Collections.Generic;
using System.Linq;
namespace Linqtutorials
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Department> objDept = new List<Department>(){
                new Department{DepId=1,DepName="Software"},
                new Department{DepId=2,DepName="Finance"},
                new Department{DepId=3,DepName="Health"}
            };
            List<Employee> objEmp = new List<Employee>()
            {
                new Employee { EmpId=1,Name = "Suresh Dasari", DeptId=1 },
                new Employee { EmpId=2,Name = "Rohini Alavala", DeptId=1 },
                new Employee { EmpId=3,Name = "Praveen Alavala", DeptId=2 },
                new Employee { EmpId=4,Name = "Sateesh Alavala", DeptId =2},
                new Employee { EmpId=5,Name = "Madhav Sai"}
};
            var result = from d in objDept
                         join e in objEmp
                         on d.DepId equals e.DeptId
                         select new
                         {
                             EmployeeName = e.Name,                            
                             DepartmentName = d.DepName
                         };
            foreach (var item in result)
            {
                Console.WriteLine(item.EmployeeName + "\t | " + item.DepartmentName);
            }
            Console.ReadLine();
        }
    }
    class Department
    {
        public int DepId { get; set; }
        public string DepName { get; set; }
    }
    class Employee
    {
        public int EmpId { get; set; }
        public string Name { get; set; }
        public int DeptId { get; set; }
    }
}

به توضیحات این مثال با دقت توجه کنید:

به کلاس Department دقت کنید این کلاس دارای دو فیلد DepId و DepName است که در Main، اشیایی از آن در یک لیست با نام objDept قرار گرفته اند.

حالا کلاس Employee را در نظر بگیرید این کلاس دارای سه فید EmpId و Name و DeptId می باشد که در Main  اشیایی از آن در یک لیست با نام objEmp قرار گرفته اند. فیلد DeptId برای ما اهمیت دارد چون در واقع خط ارتباط دو کلاس Department و Employee است. به مقادیر فیلد ها در دو لیست موجود دقت کنید.

در این مثال در اشیای  کلاس Department چند گروه شغلی با ID هایی مشخص تعریف شده اند مثلا گروه شغلی Health با ID=3 مشخص شده است. در اشیای کلاس Employee کارمندان یک سازمان که هر کدام یک ID مشخص دارند، ثبت شده و در فیلد DeptId در واقع ID شغل آن ها ثبت گردیده که اشاره به کلاس Department دارد.

مثلا رکورد EmpId=1,Name = "Suresh Dasari", DeptId=1  شخصی را با ID=1 و نام Suresh Dasari و شغل Software مشخص می کند.

زمانی را در نظر بگیرید که تعداد کارمندان و ردیف های شغلی بسیار زیاد است و قصد داریم افراد را بر اساس ردیف های شغلی دسته بندی کنیم، در اینجا نیاز به استفاده از متد Join داریم که در این مثال از آن استفاده شده است.

در این مثال ابتدا روش Query را بررسی می کنیم:

d را می توان اشاره گری به  objDept و e را اشاره گری به objEmp در نظر گرفت. با استفاده از متد Join اعلام می کنیم که دو شی به هم متصل (ربط داده) شوند.  در خط بعدی فیلد مشترک را بعد از کلمه ی on مشخص می کنیم و شرط تساوی آن را با کلمه ی equals اعلام می کنیم. خب به عبارت بعد از Select دقت کنید در انتها ما باید اعلام کنیم که اطلاعاتی که از دو جدول با هم Join شدند بر چه اساس و فیلد هایی باید به عنوان خروجی در نظر گرفته شوند.

در این مورد e.Name از کلاس Employee با نام EmployeeName(نام دلخواه) و d.DepName از کلاس Department با نام DepartmentName(نام دلخواه) خروجی مورد نظر ما است. مشاهده می کنید که در حلقه ی foreach این دو توسط item قابل دستیابی است.

تذکر:اگر قسمت EmployeeName = e.Name و DepartmentName = d.DepName را متوجه نشدید عبارت anonymous type را در اینترنت جست و جو کنید.

خروجی مثال Join
خروجی مثال Join

همان طور که در خروجی مشخص است فقط افرادی که دارای DeptId هستند، نشان داده شده اند.

برای نوشتن کد به روش Extension به صورت زیر عمل می کنیم:

var result = objDept.Join(objEmp,d =>d.DepId, e => e.DeptId,(dep,emp) =>new {EmployeeName=emp.Name,DepartmentName=dep.DepName });

خروجی این کد کاملا مشابه با حالت قبل است و فقط نوشتن آن کمی پیچیده تر است.

پیاده سازی LEFT OUTER JOIN

این پیاده سازی تمامی عناصر موجود در مجموعه ی چپ را بر اساس عناصر موجود در مجموعه ی سمت راست بر می گرداند. به مثال قبلی دقت کنید فقط اسم کارمندانی در خروجی قابل مشاهده است که فیلد DeptId آن ها دارای مقدار است. در واقع می توان این چنین تصور کرد که اولویت با نشان دادن اطلاعات مجموعه ی سمت چپ است. عکس زیر مفهوم کلی Left Outer Join را نشان می دهد.

مفهوم کلی Left Outer Join
مفهوم کلی Left Outer Join

الگوی استفاده از آن تا حدودی مانند Inner Join است اما دارای تفاوت های کوچکی است که می توانید در الگوی زیر مشاهده کنید:

var result = from e in objEmp
join d in objDept
on e.DeptId equals d.DepId into empDept
from ed in empDept.DefaultIfEmpty()
select new
{
EmployeeName = e.Name,
DepartmentName = ed == null ? "No Department" : ed.DepName
}

ابتدا به متد DefaultIfEmpty دقت کنید. در اینجا بر روی شی empDept اعمال شده این شی حاوی اطلاعات دو مجموعه بر اساس دو فیلد مشترک است. در چند خط بعدی بر روی ed که حاوی اطلاعات empDept.DefaultIfEmpty است یک شرط قرار داده شده که اگر ed.DepName دارای مقدار نبود عبارت No Department نمایش داده شود در غیر این صورت DepName مربوطه چاپ می شود. به طور کلی خروجی متد DefaultIfEmpty برای فیلد هایی که مقدار ندارند 0 و برای باقی فیلدهایی که داری مقدار هستند، برابر با مقدار خود فیلد است.

نکته: منظور از مجموعه ی سمت چپ مجموعه ای است که ابتدا نوشته می شود.

برای درک بهتر به مثال زیر توجه کنید:

using System;
using System.Collections.Generic;
using System.Linq;
namespace Linqtutorials
{
    class Program
    {
        static void Main(string[] args)
        {
            List<Department> objDept = new List<Department>(){
                new Department{DepId=1,DepName="Software"},
                new Department{DepId=2,DepName="Finance"},
                new Department{DepId=3,DepName="Health"}
            };
            List<Employee> objEmp = new List<Employee>()
            {
                new Employee { EmpId=1,Name = "Suresh Dasari", DeptId=1 },
                new Employee { EmpId=2,Name = "Rohini Alavala", DeptId=1 },
                new Employee { EmpId=3,Name = "Praveen Alavala", DeptId=2 },
                new Employee { EmpId=4,Name = "Sateesh Alavala", DeptId =2},
                new Employee { EmpId=5,Name = "Madhav Sai"}
                };
            var result = from e in objEmp
                         join d in objDept
                         on e.DeptId equals d.DepId into empDept
                         from ed in empDept.DefaultIfEmpty()
                         select new
                         {
                             EmployeeName = e.Name,
                             DepartmentName = ed == null ? "No Department" : ed.DepName
                         };
            foreach (var item in result)
            {
                Console.WriteLine(item.EmployeeName + "\t | " + item.DepartmentName);
            }
            Console.ReadLine();
        }
    }
    class Department
    {
        public int DepId { get; set; }
        public string DepName { get; set; }
    }
    class Employee
    {
        public int EmpId { get; set; }
        public string Name { get; set; }
        public int DeptId { get; set; }
    }
}

کد بالا دقیقا کد استفاده شده برای مثال اول است فقط تغییراتی کوچک در آن انجام شده تا عملیات Left Outer Join را پیاده سازی کند. کد را در IDE خود اجرا کنید و خروجی را مشاهده کنید می بینید که اسم تمامی کارمندان در خروجی نشان داده شده (مجموعه ی چپ) و مواردی که DepName آن ها مقدار ندارد با No Department مشخص شده است. به یاد دارید که در خروجی مثال اول فقط اسامی کارکنانی که دارای DepName بودند در خروجی قابل مشاهده بود.

مثال Left Outer Join
مثال Left Outer Join

نوشتن کد به صورت Query برای Left Outer Join کمی پیچیده و مشکل است و نیازمند به کارگیری متدهایی است که تا الان بررسی نشده اند به همین دلیل آن را در جایی مناسب بررسی خواهیم کرد.

پیاده سازی CROSS JOIN

در این روش از Join تمامی عناصر مجموعه ی سمت چپ به تمامی عناصر مجموعه ی سمت راست نسبت داده می شوند. به شکل زیر که نمایی کلی از Cross Join است دقت کنید:

مفهوم کلی Cross Join
مفهوم کلی Cross Join

الگوی استفاده از آن به صورت زیر است:

var result = from e in obj1
from d in obj2
select new
{
name1 = e.property,
name2 = d.property
};

به مثال زیر توجه کنید:

using System;
using System.Collections.Generic;
using System.Linq;
namespace Linqtutorials
{
class Program
{
static void Main(string[] args)
{
List<Department> objDept = new List<Department>(){
new Department{DepId=1,DepName="Software"},
new Department{DepId=2,DepName="Finance"},
new Department{DepId=3,DepName="Health"}
};
List<Employee> objEmp = new List<Employee>()
{
new Employee { EmpId=1,Name = "Suresh Dasari", DeptId=1 },
new Employee { EmpId=2,Name = "Rohini Alavala", DeptId=1 },
new Employee { EmpId=3,Name = "Praveen Kumar", DeptId=2 },
new Employee { EmpId=4,Name = "Sateesh Chandra", DeptId =2},
new Employee { EmpId=5,Name = "Madhav Sai"}
};
var result = from e in objEmp
from d in objDept
select new
{
EmployeeName = e.Name,
DepartmentName = d.DepName
};
foreach (var item in result)
{
Console.WriteLine(item.EmployeeName + "\t | " + item.DepartmentName);
}
Console.ReadLine();
}
}
class Department
{
public int DepId { get; set; }
public string DepName { get; set; }
}
class Employee
{
public int EmpId { get; set; }
public string Name { get; set; }
public int DeptId { get; set; }
}
}

همانطور که مشاهده می کنید در پیاده سازی Cross Join اصلا واژه ی Join به کار نرفته است.

مثال اول Cross Join
مثال اول Cross Join

اگر کد را اجرا کنید و خروجی را مشاهده کنید در نگاه اول شاید عجیب به نظر برسد ولی این روش پیاده سازی Join برای تولید داده های سطری و ستونی مرتب به کار می رود ولی کاربرد چندانی ندارد. به همین دلیل از توضیح بیشتر آن پرهیز می کنیم اما برای درک کلی کاربرد آن مثال جالب زیر را در نظر بگیرید:

char[] letters = "ABCDEFGH".ToCharArray();
char[] digits = "12345678".ToCharArray();
var coords =
    from l in letters
    from d in digits
    select l.ToString() + d;
foreach (var coord in coords)
{
    Console.Write("{0} ", coord);
    if (coord.EndsWith("8"))
    {
        Console.WriteLine();
    }
}
خروحی مثال دوم Cross Join
خروحی مثال دوم Cross Join

پیاده سازی GROUP JOIN

یکی از مهم ترین و پر کاربردترین پیاده سازی های Join استفاده از Group Join است. برای فهم بهتر این کاربرد توضیح آن را در قالب مثال بررسی می کنیم.

مثال:

using System;
using System.Collections.Generic;
using System.Linq;
namespace Linqtutorials
{
    class Program
    {
        static void Main(string[] args)
        {      
           List<Department> objDept = new List<Department>(){
                new Department{DepId=1,DepName="Software"},
                new Department{DepId=2,DepName="Finance"},
                new Department{DepId=3,DepName="Health"}
            };
            List<Employee> objEmp = new List<Employee>()
            {
                new Employee { EmpId=1,Name = "Suresh Dasari", DeptId=1 },
                new Employee { EmpId=2,Name = "Rohini Alavala", DeptId=1 },
                new Employee { EmpId=3,Name = "Praveen Alavala", DeptId=2 },
                new Employee { EmpId=4,Name = "Sateesh Alavala", DeptId =2},
                new Employee { EmpId=5,Name = "Madhav Sai"}
            };
            var result = from d in objDept                        
                         join e in objEmp on d.DepId equals e.DeptId into empDept
                         select new
                         {
                             DepartmentName = d.DepName,
                             Employees = from emp2 in empDept
                                         orderby emp2.Name
                                         select emp2
                         };
            foreach (var empGroup in result)
            {
                Console.WriteLine(empGroup.DepartmentName);
                foreach (var item in empGroup.Employees)
                {
                    Console.WriteLine("    {0}", item.Name);
                }
            }
            Console.ReadLine();
        }
    }
    class Department
    {
        public int DepId { get; set; }
        public string DepName { get; set; }
    }
    class Employee
    {
        public int EmpId { get; set; }
        public string Name { get; set; }
        public int DeptId { get; set; }
    }
}

این مثال دقیقا مانند مثال بررسی شده در قسمت Left Outer Join است، فقط نوع نمایش خروجی آن منظم تر است. توجه داشته باشید تا خط کد select new کاملا مشابه قبل پیاده سازی شده اما مهم ترین بخشی که باعث می شود داده ها به صورت منظم در خروجی نمایان شوند Query دومی است که حاصل آن در Employees ذخیره می شود. لازم به ذکر است نام Employees کاملا اختیاری بوده و قابل تغییر است. در این مثال از empDept یک Query گرفتیم و با استفاده از متد orderby نام کارکنان را مرتب سازی کرده ایم که در نتیجه ی آن خروجی منظم خواهیم داشت. به شکل زیر که خروجی این مثال است توجه کنید و با خروجی حاصل شده از مثال مربوط به Left Outer Join مقایسه کنید.

خروجی مثال Group Join
خروجی مثال Group Join

همان طور که مشاهده می کنید هر ردیف شغلی با کارمندان بخش خود به صورت مرتب در خروجی نشان داده شده اند. حالا به قسمتی از کد که foreach در آن به کار رفته، دقت کنید. وظیفه ی این بخش چاپ صحیح و مرتب اطلاعاتی است که در result ذخیره شده اند. حلقه ی اول وظیفه ی چاپ DepartmentName را دارد.

به پیمایش کننده ی empGroup دقت کنید که به دو شی DepartmentName و Employees دسترسی دارد. DepartmentName از نوع string است که توسط حلقه ی اول پیمایش و چاپ می شود اما شی Employees از نوع Query بوده پس برای چاپ آن نیاز به یک حلقه ی داخلی است تا اطلاعات آن را پیمایش و چاپ کند.

این قسمت از آموزش هم به پایان رسید. توجه داشته باشید مبحث Join کمی مشکل بوده و توضیح کاربرد آن ها مشکل تر و نیاز به حل مثال هایی بیشتر دارد پس بهترین راه ممکن برای یادگیری عمیق آن حل تمرین بسیار است.

موفق باشید.

تمام فصل‌های سری ترتیبی که روکسو برای مطالعه‌ی دروس سری آموزش LINQ توصیه می‌کند:
نویسنده شوید
دیدگاه‌های شما

در این قسمت، به پرسش‌های تخصصی شما درباره‌ی محتوای مقاله پاسخ داده نمی‌شود. سوالات خود را اینجا بپرسید.