با توجه به اصل Dry تا میتوان باید از نوشتن کدهای تکراری خودداری کرد و کدها را تا جایی که ممکن است به قسمت هایی با قابلیت استفادهی مجدد تبدیل کرد. حین کار کردن با ORMهای معروف مثل NHibernate و EntityFramework زمان زیادی نوشتن کوئریها جهت واکشی دادهها از دیتابیس صرف میشود. اگر بتوان کوئری هایی با قابلیت استفادهی مجدد نوشت علاوه بر کاهش زمان توسعه قابلیت هایی قدرتمندی مانند زنجیر کردن کوئریها به دنبال هم به دست میآید.
با یک مثال نحوهی نوشتن و مزایای کوئری با قابلیت استفادهی مجدد را بررسی میکنیم :
برای مثال دو جدول شهرها و دانش آموزان را درنظر بگیرید:
namespace ReUsableQueries.Model { public class Student { public int Id { get; set; } public string Name { get; set; } public string LastName { get; set; } public int Age { get; set; } [ForeignKey("BornInCityId")] public virtual City BornInCity { get; set; } public int BornInCityId { get; set; } } public class City { public int Id { get; set; } public string Name { get; set; } public virtual ICollection<Student> Students { get; set; } } }
در ادامه این کلاسها را در معرض دید EF Code first قرار داده:
using System.Data.Entity; using ReUsableQueries.Model; namespace ReUsableQueries.DAL { public class MyContext : DbContext { public DbSet<City> Cities { get; set; } public DbSet<Student> Students { get; set; } } }
و همچنین تعدادی رکورد آغازین را نیز به جداول مرتبط اضافه میکنیم:
public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { var city1 = new City { Name = "city-1" }; var city2 = new City { Name = "city-2" }; context.Cities.Add(city1); context.Cities.Add(city2); var student1 = new Student() {Name = "Shaahin",LastName = "Kiassat",Age=22,BornInCity = city1}; var student2 = new Student() { Name = "Mehdi", LastName = "Farzad", Age = 31, BornInCity = city1 }; var student3 = new Student() { Name = "James", LastName = "Hetfield", Age = 49, BornInCity = city2 }; context.Students.Add(student1); context.Students.Add(student2); context.Students.Add(student3); base.Seed(context); } }
فرض کنید قرار است یک کوئری نوشته شود که در جدول دانش آموزان بر اساس نام ، نام خانوادگی و سن جستجو کند :
var context = new MyContext(); var query= context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where( x => x.Age == age);
احتمالا هنوز کسانی هستند که فکر میکنند کوئریهای LINQ همان لحظه که تعریف میشوند اجرا میشوند اما اینگونه نیست . در واقع این کوئری فقط یک Expression از رکوردهای جستجو شده است و تا زمانی که متد ToList یا ToArray روی آن اجرا نشود هیچ داده ای برگردانده نمیشود.
در یک برنامهی واقعی دادههای باید به صورت صفحه بندی شده و مرتب شده برگردانده شود پس کوئری به این صورت خواهد بود :
var query= context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where( x => x.Age == age).OrderBy(x=>x.LastName).Skip(skip).Take(take);
ممکن است بخواهیم در متد دیگری در لیست دانش آموزان بر اساس نام ، نام خانوادگی ، سن و شهر جستجو کنیم و سپس خروجی را اینبار بر اساس سن مرتب کرده و صفحه بندی نکنیم:
var query = context.Students.Where(x => x.Name.Contains(name)).Where(x => x.LastName.Contains(lastName)).Where ( x => x.Age == age).Where(x => x.BornInCityId == 1).OrderBy(x => x.Age);
همانطور که میبینید قسمت هایی از این کوئری با کوئری هایی که قبلا نوشتیم یکی است ، همچنین حتی ممکن است در قسمت دیگری از برنامه نتیجهی همین کوئری را به صورت صفحه بندی شده لازم داشته باشیم.
اکنون نوشتن این کوئریها میان کد های Business Logic باعث شده هیچ استفادهی مجددی نتوانیم از این کوئریها داشته باشیم. حال بررسی میکنیم که چگونه میتوان کوئری هایی با قابلیت استفادهی مجدد نوشت :
namespace ReUsableQueries.Quries { public static class StudentQueryExtension { public static IQueryable<Student> FindStudentsByName(this IQueryable<Student> students,string name) { return students.Where(x => x.Name.Contains(name)); } public static IQueryable<Student> FindStudentsByLastName(this IQueryable<Student> students, string lastName) { return students.Where(x => x.LastName.Contains(lastName)); } public static IQueryable<Student> SkipAndTake(this IQueryable<Student> students, int skip , int take) { return students.Skip(skip).Take(take); } public static IQueryable<Student> OrderByAge(this IQueryable<Student> students) { return students.OrderBy(x=>x.Age); } } }
همان طور که مشاهده میکنید به کمک متدهای الحاقی برای شیء IQueryable<Student> چند کوئری نوشته ایم . اکنون در محل استفاده از کوئریها میتوان این کوئریها را به راحتی به هم زنجیر کرد. همچنین اگر روزی قرار شد منطق یکی از کوئریها عوض شود با عوض کردن آن در یک قسمت برنامه همه جا اعمال میشود. نحوهی استفاده از این متدهای الحاقی به این صورت خواهد بود :
var query = context.Students.FindStudentsByName(name).FindStudentsByLastName(lastName).SkipAndTake(skip,take);
فرض کنید قرار است یک سیستم جستجوی پیشرفته به برنامه اضافه شود که بر اساس شرطهای مختلف باید یک شرط در کوئری اعمال شود یا نشود ، به کمک این طراحی جدید به راحتی میتوان بر اساس شرطهای مختلف یک کوئری را اعمال کرد یا نکرد :
var query = context.Students.AsQueryable(); if (searchByName) { query= query.FindStudentsByName(name); } if (orderByAge) { query = query.OrderByAge(); } if (paging) { query = query.SkipAndTake(skip, take); } return query.ToList();
همچنین این کوئریها وابسته به ORM خاصی نیستند البته این نکته هم مد نظر است که LINQ Provider بعضی ORMها ممکن است بعضی کوئریها را پشتیبانی نکند.