۱۳۸۹/۰۸/۱۳

نحوه‌ي صحيح فراخواني SQL Aggregate Functions حين استفاده از LINQ


SQL Aggregate Functions كه مد نظر شما هستند مانند Min ، Max ، Sum و امثال آن. بحث LINQ هم زمانيكه از الگوي Repository استفاده شود مستقل از نوع ORM مورد نظر خواهد شد؛ بنابراين در اينجا مقصود از LINQ مي‌تواند LINQ to SQL ، LINQ to Entities ، LINQ to NHibernate و كلا هر نوع ORM ديگري با پشتيباني از LINQ باشد.
صورت مساله هم اين است: آيا نوشتن عبارت LINQ ايي به شكل زير صحيح است؟
decimal amount = respository.Transactions
.Where(t=>t.TransactionDate>new DateTime(2010,10,13))
.Sum(t=>t.Amount);
پاسخ: خير!
توضيحات:
عبارت LINQ فوق در نهايت به شكل زير ترجمه خواهد شد:
-- Region Parameters
-- @p0: DateTime [2010/10/13 12:00:00 ق.ظ]
-- EndRegion
SELECT SUM([t0].[Amount]) AS [value]
FROM [Transactions] AS [t0]
WHERE [t0].[TransactionDate] > @p0
و اتفاقا در اين سيستم پس از تاريخ 2010/10/13 هيچ تراكنشي ثبت نشده است؛ بنابراين خروجي اين كوئري null خواهد بود و نه صفر. همينجا است كه يكي از استثناهاي زير صادر شده و ادامه‌ي برنامه با مشكل مواجه خواهد شد:
- System.InvalidOperationException: The cast to value type 'decimal' failed because the materialized value is null.
- InvalidOperationException: The null value cannot be assigned to a member with type decimal which is a non-nullable value type.

مشكل هم از اينجا ناشي مي‌شود كه متغييري از نوع deciaml يا int و امثال آن، مقدار دريافتي نال را نمي‌پذيرند. براي رفع اين مشكل بايد عبارت LINQ فوق به صورت زير بازنويسي شود (و اهميتي هم ندارد كه Sum است يا Max يا Avg و غيره؛ در مورد بكارگيري تمام SQL Aggregate Functions در يك عبارت LINQ ، اين مورد بايد لحاظ گردد):
decimal amount = respository.Transactions
.Where(t=>t.TransactionDate>new DateTime(2010,10,13))
.Sum(t=>(decimal?)t.Amount)??0;

دقيقا به همين علت است كه در دات نت، nullable types تعريف شده‌اند. امكان ذخيره سازي null‌ در يك متغير براي مثال از نوع decimal وجود ندارد اما نوع decimal? (و يا Nullable<decimal> به بياني ديگر) اين قابليت را دارد.
شايد بگوئيد كه در اينجا با تغيير تعريف متغير به decimal? amount مشكل حل مي‌شود، اما خير. تعريف extension method مربوط به sum به صورت زير است:

public static TResult Sum<TSource>(
this IQueryable<TSource> source,
Expression<Func<TSource, TResult>> selector)

در اين تعريف به TResult دقت نمائيد؛ هم بيانگر نوع خروجي نهايي متد و هم مشخص سازنده‌ي نوع پارامتري است كه خروجي Lambda Expression را تشكيل مي‌دهد. به اين معنا كه سي شارپ، TResult را از lambda expression دريافت كرده و خروجي Sum را بر همان مبنا و نوع تشكيل مي‌دهد. بنابراين براي دريافت خروجي nullable بايد TResult ايي nullable را همانند مثال فوق ايجاد كنيم.

خلاصه بحث:
اگر در كدهاي LINQ خود كه با بانك اطلاعاتي سر و كار دارند از معادل‌هاي SQL Aggregate Functions استفاده كرده‌ايد، آن‌ها را يافته و نكته‌ي nullable TResult فوق را به آن‌ها اعمال كنيد؛ در غير اينصورت منتظر باشيد تا روزي برنامه شما به سادگي كرش كند.