۱۳۸۹/۰۸/۰۷

تفاوت بين IQueryable و IEnumerable در حين كار با ORMs


متد زير را كه يكي از اشتباهات رايج حين استفاده از LINQ خصوصا جهت Binding اطلاعات است، در نظر بگيريد:
IQueryable<Customer> GetCustomers()

اين متد در حقيقت هيچ چيزي را Get نمي‌كند! نام اصلي آن GetQueryableCustomers و يا GetQueryObjectForCustomersاست.
IQueryable قلب LINQ است و تنها بيانگر يك عبارت (expression) از ركوردهايي مي‌باشد كه مد نظر شما است و نه بيشتر.
IQueryable<Customer> youngCustomers = repo.GetCustomers().Where(m => m.Age < 15);
براي مثال زمانيكه يك IQueryable را همانند مثال فوق فيلتر مي‌كنيد نيز هنوز چيزي از بانك اطلاعاتي يا منبع داده‌اي دريافت نشده است. هنوز هيچ اتفاقي رخ نداده است و هنوز رفت و برگشتي به منبع داده‌اي صورت نگرفته است.
به آن بايد به شكل يك expression builder نگاه كرد و نه ليستي از اشياء فيلتر شده‌ي ما. به اين مفهوم، deferred execution (اجراي به تاخير افتاده) نيز گفته مي‌شود (بايد دقت داشت كه IQueryable هم يك نوع IEnumerable است به علاوه expression trees كه مهم‌ترين وجه تمايز آن نيز مي‌باشد).
براي مثال در عبارت زير تنها در زمانيكه متد ToList فراخواني مي‌شود، كل عبارت LINQ ساخته شده، به عبارت SQL متناظر با آن ترجمه شده، اطلاعات از ديتابيس اخذ گرديده و حاصل به صورت يك ليست بازگشت داده مي‌شود:
IList<Competitor> competitorRecords =  competitorRepository
.Competitors
.Where(m => !m.Deleted)
.OrderBy(m => m.countryId)
.ToList(); //فقط اينجا است كه اس كيوال نهايي توليد مي‌شود

در مورد IEnumerable ها چطور؟
IEnumerable<Product> products = repository.GetProducts();
var productsOver25 = products.Where(p => p.Cost >= 25.00);
دو سطر فوق به اين معنا است:
لطفا ابتدا به بانك اطلاعاتي رجوع كن و تمام ركوردهاي محصولات موجود را بازگشت بده. سپس بر روي اين حجم بالاي اطلاعات، محصولاتي را كه قيمت بالاي 25 دارند، فيلتر كن.

اگر همين دو سطر را با IQueryable بازنويسي كنيم چطور؟
 IQueryable<Product> products = repository.GetQueryableProducts();
var productsOver25 = products.Where(p => p.Cost >= 25.00);
در سطر اول تنها يك عبارت LINQ ساخته شده است و بس. در سطر دوم نيز به همين صورت. در طي اين دو سطر حتي يك رفت و برگشت به بانك اطلاعاتي صورت نخواهد گرفت. در ادامه اگر اين اطلاعات به نحوي Select شوند (يا ToList فراخواني شود، يا در طي يك حلقه براي مثال Iteration ايي روي اين حاصل صورت گيرد يا موارد مشابه ديگر)، آنگاه كوئري SQL متناظر با عبارت LINQ فوق ساخته شده و بر روي بانك اطلاعاتي اجرا خواهد شد.
بديهي است اين روش منابع كمتري را نسبت به حالتي كه تمام اطلاعات ابتدا دريافت شده و سپس فيلتر مي‌شوند، مصرف مي‌كند (حالت بازگشت تمام اطلاعات ممكن است شامل 20000 ركورد باشد، اما حالت دوم شايد فقط 5 ركورد را بازگشت دهد).

سؤال: پس IQueryable بسيار عالي است و از اين پس كلا از IEnumerable ها ديگر نبايد استفاده كرد؟
خير! توصيه اكيد طراحان اين است كه لطفا تا حد امكان متدهايي كه IQueryable بازگشت مي‌دهند ايجاد نكنيد! IQueryable يعني اينكه اين نقطه‌ي آغازين كوئري در اختيار شما، بعد برو هر كاري كه دوست داشتي با آن در طي لايه‌هاي مختلف انجام بده و هر زمانيكه دوست داشتي از آن يك خروجي تهيه كن. خروجي IQueryable به معناي مشخص نبودن زمان اجراي نهايي كوئري و همچنين مبهم بودن نحوه‌ي استفاده از آن است. به همين جهت متدهايي را طراحي كنيد كه IEnumerable بازگشت مي‌دهند اما در بدنه‌ي آن‌ها به نحو صحيح و مطلوبي از IQueryable استفاده شده است. به اين صورت حد و مرز يك متد كاملا مشخص مي‌شود. متدي كه واقعا همان فيلتر كردن محصولات را انجام مي‌دهد، همان 5 ركورد را بازگشت خواهد داد؛ اما با استفاده از يك ليست يا يك IEnumerable و نه يك IQueryable كه پس از فراخواني متد نيز به هر نحو دلخواهي قابل تغيير است.