مطالب
دستکاری کردن عملیات Sort در SQL Server
گاهی اوقات لازم می‌باشد، در زمان Sort نمودن یک ستون، تمایل داشته باشیم Range خاصی از مقادیر آن ستون در ابتدا قرار گیرد، و عملیات Sort پس از آن Range، اعمال گردد. برای انجام چنین کاری می‌توانیداز روش زیر استفاده نمایید:
برای درک مطلب مثالی می‌زنیم:
در ابتدا Script زیر را اجرا نمایید، که شامل یک جدول و درج چند رکورد در آن می‌باشد:
Create Table TestSort
(ID  int identity(1,1),
Name nvarchar(30),
Color nvarchar(15)
)
درج رکورد:
Insert into TestSort (Name,color)
Values  ('Adjustable Race',null)
       ,('Bearing Ball',null)
       ,('Headset Ball Bearings',null)
       ,('LL Crankarm','Black')
       ,('ML Crankarm','Black')
       ,('Chainring','Black')
       ,('Front Derailleur Cage','Silver')
       ,('Front Derailleur Linkage','Silver')
       ,('Lock Ring','Silver')
       ,('HL Road Frame - Red, 62','Red')
       ,('HL Road Frame - Red, 48','Red')
       ,('LL Road Frame - Red, 44','Red')
       ,('Full-Finger Gloves, M','RED')
       ,('Road-550-W Yellow, 38','Yellow')
       ,('Road-550-W Yellow, 40','Yellow')
       ,('Road-550-W Yellow, 42','Yellow')
       ,('Classic Vest, S','Blue')
       ,('Classic Vest, M','Blue')
       ,('Classic Vest, L','Blue')
در جدول TestSort ستونی به نام Color داریم، که نام رنگها در آن درج شده است، رنگهایی همچون  Black ، Silver،Blue،Yellow و Red
درابتدا روی ستون Color بصورت نرمال Sort صعودی انجام می‌دهیم:
Select t.ID,t.Name,t.Color from TestSort as t order by t.Color
خروجی:

مطابق شکل،زمانی که Sort بصورت صعودی است، رکوردهایی را که ستون Color آنها دارای مقدار Null می‌باشند، در ابتدای جدول قرار گرفته اند.
در ادامه می‌خواهیم، عملیات Sort ی را روی ستون Color انجام دهیم، بطوریکه تمامی رکوردهایی که مقدار ستون Color شان Red است، در ابتدای جدول قرار گیرد، و پس از آن عملیات سورت روی رنگهای دیگر اعمال شود.
برای انجام چنین کاری کافیست Script زیر را اجرا نمایید:
Select t.ID,t.Name,t.Color from TestSort as t 
Where t.color is not null
order by  Case t.Color
When 'Red' Then Null
Else t.color 
End;
خروجی:

چطور اینکار انجام شد:
اگر بهScript ذکر شده دقت نمایید، در قسمت Order by اشاره کردیم، تمام مقادیر Red در ستون Color به Null تغییر کنند، بنابراین SQL Server، در ابتدا مقادیر Red را یافته آنها را به Null تغییر و سپس عملیات سورت را انجام می‌دهد،
برای درک بیشتر عملیاتی را که SQL Server پشت صحنه انجام می‌دهد با Script  زیر قابل شبیه سازی میباشد:
Select * into Simulation from 
(Select t.ID,t.Name,t.Color,
Case t.Color
When 'Red' Then Null
Else t.color 
End RedNull
from TestSort as t 
Where t.color is not null) A
سپس روی ستون RedNull از جدول Simulation سورت انجام می‌دهیم:
Select * from Simulation order by Rednull
خروجی:

مطابق شکل،پشت صحنه SQL Server چنین کاری را انجام می‌دهد، و در زمان نمایش ستون RedNull پنهان یا حذف می‌گردد، و ستون Color، Name و ID نمایش داده می‌شود.
امیدوارم مفید واقع شده باشد.
مطالب
آموزش MDX Query - قسمت شانزدهم – استفاده از تابع Filter در MDX Query ها

 در این قسمت بر روی تابع Filter در MDX Query ها تمرکز خواهیم کرد. برای آشنایی با این تابع یک سری از کوئری‌ها را اجرا کرده و به بررسی آنها می‌پردازیم.

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
[Product].[Product Categories].[Category] on rows
From [Adventure Works]

دقت کنید که در واکشی، مقدار فروش اینترنتی Component برابر  Null می‌باشد.

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
[Measures].[Internet Sales Amount] > 0
) on rows
From [Adventure Works]

با توجه به شرطی که اعمال شده است، فقط دسته بندی محصولاتی در خروجی می  آید که فروش اینترنتی آنها بیشتر از صفر  یا برابر Null نباشند.

استفاده از کلید واژه ی  Having  در هر محور کاری شبیه به انجام عمل فیلترینگ می‌ باشد .

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
[Product].[Product Categories].[Category]
having [Measures].[Internet Sales Amount] > 0 on rows
From [Adventure Works]

اگر بخواهیم میزان فروش اینترنتی و میزان فروش نمایندگان فروش را برای محصولاتی واکشی کنیم که میزان فروش اینترنتی آنها بیش از 500000 دلار می‌باشد ، کوئری زیر را خواهیم داشت :

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
[Measures].[Internet Sales Amount] > 500000
  ) on rows
From [Adventure Works]

و برای ایجاد شرط ترکیبی بر روی شاخص، به صورت زیر عمل خواهیم کرد :

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
[Measures].[Internet Sales Amount] > 500000
and
[Measures].[Internet Sales Amount] < 750000
) on rows
From [Adventure Works]

در مثال بالا، دسته بندی محصولاتی در خروجی واکشی شده اند که میزان فروش اینترنتی آنها بیش از 500 هزار و کمتر از 750 هزار می‌باشد.

استفاده از And , Or در شروط ترکیبی مجاز می‌باشد 

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
[Measures].[Internet Sales Amount] > 750000
or
[Measures].[Internet Sales Amount] < 500000
  ) on rows
From [Adventure Works]

در زیر با استفاده از And، شرط برروی میزان فروش نمایندگان فروش نیز قرارداده شده است.

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
(
[Measures].[Internet Sales Amount] > 750000
or
[Measures].[Internet Sales Amount] < 500000
)
and
[Measures].[Reseller Sales Amount] < 15000000
) on rows
From [Adventure Works]

در هنگام ایجاد شروط ترکیبی حتما از ()  استفاده کنید .

حال می‌خواهیم دو شاخص در یک ردیف، با یکدیگر مقایسه شوند و در صورت صحت شرط، آن ردیف در خروجی قرار گیرد

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
[Measures].[Internet Sales Amount]
>
[Measures].[Reseller Sales Amount]
) on rows
From [Adventure Works]

ایجاد فیلترینگ با استفاده از currentmember  و عملگر  Is .

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
[Product].[Product Categories].currentmember
is
[Product].[Product Categories].[Category].[Bikes]
) on rows
From [Adventure Works]

البته در  مثال بالا  می توانیم به جای استفاده از Is از = استفاده کنیم. تا اینجا عمل Filtering  برروی شاخص‌ها انجام شده است. اما امکان اعمال Filter روی Dimension ‌ها نیز وجود دارد.

کوئری زیر را بررسی کنید :

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
[Product].[Product Categories].currentmember
is
[Product].[Product Categories].[Category].[Bikes]
or
[Product].[Product Categories].currentmember
is
[Product].[Product Categories].[Category].[Accessories]
) on rows
From [Adventure Works]

در کوئری بالا میزان فروش نمایندگان فروش و فروش اینترنتی برای دسته بندی‌های Bike  و Accessories واکشی شده است.

امکان ایجاد شرایط ترکیبی از شاخص‌ها و بعد‌ها در یک Filter نیز وجود دارد.

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
(
[Product].[Product Categories].currentmember
is
[Product].[Product Categories].[Category].[Bikes]
and
[Measures].[Reseller Sales Amount] > 1000000
)
or
(
[Product].[Product Categories].currentmember
is
[Product].[Product Categories].[Category].[Accessories]
)
and
[Measures].[Reseller Sales Amount] > 750000
)on rows
From [Adventure Works]

همچنین می‌توان از Not  درون شرط  Filter  استفاده کرد

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
filter(
[Product].[Product Categories].[Category],
not
(
[Product].[Product Categories].currentmember
is
[Product].[Product Categories].[Category].[Clothing]
)
) on rows
From [Adventure Works]

در زیر می‌خواهیم به بررسی تابع non Empty بپردازیم . برای این منظور در ابتدا کوئری زیر را اجرا کنید :

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
[Product].[Product Categories].[Subcategory] on rows
From [Adventure Works]

با استفاده از تابع Non Empty، ردیف ‌ها یا ستون هایی حذف می‌ گردند که تمامی مقادیر آن‌ها ، در آن ردیف یا در آن ستون برابر  Null  باشند.

Select
{
[Measures].[Internet Sales Amount],
[Measures].[Reseller Sales Amount]
} on columns,
non empty [Product].[Product Categories].[Subcategory] on rows
From [Adventure Works]

در قسمت آینده برروی توابع Count , Top و ... تمرکز خواهیم کرد.
مطالب
طراحی یک گرید با Angular و ASP.NET Core - قسمت اول - پیاده سازی سمت سرور
یکی از نیازهای مهم هر برنامه‌ای، امکانات گزارشگیری و نمایش لیستی از اطلاعات است. به همین جهت در طی چند قسمت، قصد داریم یک گرید ساده را به همراه امکانات نمایش، صفحه بندی و مرتب سازی اطلاعات، تنها به کمک امکانات توکار Angular و ASP.NET Core تهیه کنیم.




تهیه مقدمات سمت سرور

مدلی که در تصویر فوق نمایش داده شده‌است، در سمت سرور چنین ساختاری را دارد:
namespace AngularTemplateDrivenFormsLab.Models
{
    public class Product
    {
        public int ProductId { set; get; }
        public string ProductName { set; get; }
        public decimal Price { set; get; }
        public bool IsAvailable { set; get; }
    }
}

همچنین یک منبع ساده درون حافظه‌ای را نیز جهت بازگشت 1500 محصول تهیه کرده‌ایم. علت اینجا است که ساختار نهایی اطلاعات آن شبیه به ساختار اطلاعات حاصل از ORMها باشد و همچنین به سادگی قابلیت اجرا و بررسی را داشته باشد:
using System.Collections.Generic;

namespace AngularTemplateDrivenFormsLab.Models
{
    public static class ProductDataSource
    {
        private static readonly IList<Product> _cachedItems;
        static ProductDataSource()
        {
            _cachedItems = createProductsDataSource();
        }

        public static IList<Product> LatestProducts
        {
            get { return _cachedItems; }
        }

        private static IList<Product> createProductsDataSource()
        {
            var list = new List<Product>();
            for (var i = 0; i < 1500; i++)
            {
                list.Add(new Product
                {
                    ProductId = i + 1,
                    ProductName = "نام " + (i + 1),
                    IsAvailable = (i % 2 == 0),
                    Price = 1000 + i
                });
            }
            return list;
        }
    }
}


مشخص کردن قرارداد اطلاعات دریافتی از سمت کلاینت

زمانیکه کلاینت Angular برنامه، اطلاعاتی را به سمت سرور ارسال می‌کند، یک چنین ساختاری را دریافت خواهیم کرد:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
درخواست، به یک اکشن متد مشخص ارسال شده‌است و حاوی یک سری کوئری استرینگ مشخص کننده‌ی نام خاصیت یا فیلدی که قرار است مرتب سازی بر اساس آن صورت گیرد، صعودی و نزولی بودن این مرتب سازی، شماره صفحه‌ی درخواستی و تعداد آیتم‌های در هر صفحه، می‌باشد.
بنابراین اینترفیسی را دقیقا بر اساس نام کلیدهای همین کوئری استرینگ‌ها تهیه می‌کنیم:
    public interface IPagedQueryModel
    {
        string SortBy { get; set; }
        bool IsAscending { get; set; }
        int Page { get; set; }
        int PageSize { get; set; }
    }


کاهش کدهای تکراری صفحه بندی اطلاعات در سمت سرور

با تعریف این اینترفیس چند هدف را دنبال خواهیم کرد:
الف) استاندارد سازی نام خواصی که مدنظر هستند و اعمال یک دست آن‌ها به ViewModelهایی که قرار است از سمت کلاینت دریافت شوند:
    public class ProductQueryViewModel : IPagedQueryModel
    {
        // ... other properties ...

        public string SortBy { get; set; }
        public bool IsAscending { get; set; }
        public int Page { get; set; }
        public int PageSize { get; set; }
    }
برای مثال در اینجا یک ViewModel مخصوص Product را ایجاد کرده‌ایم که می‌تواند شامل یک سری فیلد دیگر نیز باشد. اما یک سری خواص مرتب سازی و صفحه بندی آن، یک دست و مشخص هستند.

ب) امکان استفاده‌ی از این قرارداد در متدهای کمکی که نوشته خواهند شد:
    public static class IQueryableExtensions
    {
        public static IQueryable<T> ApplyPaging<T>(
          this IQueryable<T> query, IPagedQueryModel model)
        {
            if (model.Page <= 0)
            {
                model.Page = 1;
            }

            if (model.PageSize <= 0)
            {
                model.PageSize = 10;
            }

            return query.Skip((model.Page - 1) * model.PageSize).Take(model.PageSize);
        }
    }
در حین ارائه‌ی اطلاعات نهایی صفحه بندی شده به کلاینت، همیشه یک قسمت Skip و Take وجود خواهند داشت. این متدها نیز باید بر اساس یک سری خاصیت و مقدار مشخص، مانند صفحه شماره صفحه‌ی جاری و تعداد ردیف‌های در هر صفحه کار کنند. اکنون که قرارداد IPagedQueryModel را تهیه کرده‌ایم و ViewModel ما نیز آن‌را پیاده سازی می‌کند، مطمئن خواهیم بود که می‌توان به سادگی به این خواص دسترسی یافت و همچنین این کد تکراری صفحه بندی را توانسته‌ایم به یک متد الحاقی کمکی منتقل و حجم کدهای نهایی را کاهش دهیم.
همچنین دراینجا بجای صدور استثناء در حین دریافت مقادیر غیرمعتبر شماره صفحه یا تعداد ردیف‌های هر صفحه، از حالت «بخشنده» بجای حالت «تدافعی» استفاده شده‌است. برای مثال در حالت «بخشنده» اگر شماره صفحه منفی بود، همان صفحه‌ی اول اطلاعات نمایش داده می‌شود؛ بجای صدور یک استثناء (یا حالت «تدافعی و defensive programming»).


کاهش کدهای تکراری مرتب سازی اطلاعات در سمت سرور

همانطور که عنوان شد، از سمت کلاینت، چنین لینکی را دریافت خواهیم کرد:
 http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
در اینجا، هربار sortBy و isAscending می‌توانند متفاوت باشند و در نهایت به یک چنین کدهایی خواهیم رسید:
if(model.SortBy == "f1")
{
   query = !model.IsAscending ? query.OrderByDescending(x => x.F1) : query.OrderBy(x => x.F1);
}
امکان نوشتن این نوع کوئری‌ها توسط قابلیت تعریف زنجیره‌وار کوئری‌های LINQ میسر است و در نهایت زمانیکه ToList نهایی فراخوانی می‌شود، آنگاه است که کوئری SQL معادل این‌ها تولید خواهد شد.
اما در این حالت نیاز است به ازای تک تک فیلدها، یکبار if/else یافتن فیلد و سپس بررسی صعودی و نزولی بودن آن‌ها صورت گیرد که در نهایت ظاهر خوشایندی را نخواهند داشت.

یک نمونه از مزیت‌های تهیه‌ی قرارداد IPagedQueryModel را در حین نوشتن متد ApplyPaging مشاهده کردید. نمونه‌ی دیگر آن کاهش کدهای تکراری مرتب سازی اطلاعات است:
namespace AngularTemplateDrivenFormsLab.Utils
{
    public static class IQueryableExtensions
    {
        public static IQueryable<T> ApplyOrdering<T>(
          this IQueryable<T> query,
          IPagedQueryModel model,
          IDictionary<string, Expression<Func<T, object>>> columnsMap)
        {
            if (string.IsNullOrWhiteSpace(model.SortBy) || !columnsMap.ContainsKey(model.SortBy))
            {
                return query;
            }

            if (model.IsAscending)
            {
                return query.OrderBy(columnsMap[model.SortBy]);
            }
            else
            {
                return query.OrderByDescending(columnsMap[model.SortBy]);
            }
        }
    }
}
در اینجا متد الحاقی ApplyOrdering، کار دریافت و بررسی خواص مدنظر را از طریق یک دیکشنری انجام می‌دهد و مابقی کدهای تکراری نوشته شده، حذف خواهند شد. برای نمونه، مثالی از نحوه‌ی استفاده‌ی از این متد الحاقی را در ذیل مشاهده می‌کنید:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
query = query.ApplyOrdering(queryModel, columnsMap);
ابتدا نگاشتی بین خواص رشته‌ای دریافتی از سمت کلاینت، با خواص شیء Product برقرار شده‌است. سپس این نگاشت به متد ApplyOrdering ارسال شده‌است. به این ترتیب از نوشتن تعداد زیادی if/else یا switch بر اساس خاصیت SortBy بی‌نیاز شده‌ایم، حجم کدهای نهایی تولیدی کاهش پیدا می‌کنند و برنامه نیز خواناتر می‌شود.


تهیه قرارداد ساختار اطلاعات بازگشتی از سمت سرور به سمت کلاینت

تا اینجا قرارداد اطلاعات دریافتی از سمت کلاینت را مشخص کردیم. همچنین از آن برای ساده سازی عملیات مرتب سازی و صفحه بندی اطلاعات کمک گرفتیم. در ادامه نیاز است مشخص کنیم چگونه می‌خواهیم این اطلاعات را به سمت کلاینت ارسال کنیم:
using System.Collections.Generic;

namespace AngularTemplateDrivenFormsLab.Models
{
    public class PagedQueryResult<T>
    {
        public int TotalItems { get; set; }
        public IEnumerable<T> Items { get; set; }
    }
}
عموما ساختار اطلاعات صفحه بندی شده، شامل تعداد کل آیتم‌های تمام صفحات (خاصیت TotalItems) و تنها اطلاعات ردیف‌های صفحه‌ی جاری درخواستی (خاصیت Items) است و چون در اینجا این Items از هر نوعی می‌تواند باشد، بهتر است آن‌را جنریک تعریف کنیم.


پایان کار بازگشت اطلاعات سمت سرور با تهیه اکشن متد GetPagedProducts

در اینجا اکشن متدی را مشاهده می‌کنید که اطلاعات نهایی مرتب سازی شده و صفحه بندی شده را بازگشت می‌دهد:
    [Route("api/[controller]")]
    public class ProductController : Controller
    {
        [HttpGet("[action]")]
        public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
        {
            var pagedResult = new PagedQueryResult<Product>();

            var query = ProductDataSource.LatestProducts
                                         .AsQueryable();

            //TODO: Apply Filtering ... .where(p => p....) ...

            var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
            query = query.ApplyOrdering(queryModel, columnsMap);

            pagedResult.TotalItems = query.Count();
            query = query.ApplyPaging(queryModel);
            pagedResult.Items = query.ToList();
            return pagedResult;
        }
    }
توضیحات تکمیلی

امضای این اکشن متد، شامل دو مورد مهم است:
 public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
الف) ViewModel ایی که پیاده سازی کننده‌ی IPagedQueryModel است. به این ترتیب می‌توان به ساختار استانداردی از مقادیر مورد نیاز برای صفحه بندی و مرتب سازی رسید و همچنین این ViewModel می‌تواند حاوی خواص اضافی ویژه‌ی خود نیز باشد.
ب) خروجی آن از نوع PagedQueryResult است که در مورد آن توضیح داده شد. بنابراین باید به همراه تعداد کل رکوردهای جدول محصولات و همچنین تنها آیتم‌های صفحه‌ی جاری درخواستی باشد.

در ابتدای کار، دسترسی به منبع داده‌ی درون حافظه‌ای ابتدای برنامه را مشاهده می‌کنید. برای اینکه کارکرد آن‌را شبیه به کوئری‌های ORMها کنیم، یک AsQueryable نیز به انتهای آن اضافه شده‌است.
 var query = ProductDataSource.LatestProducts
  .AsQueryable();

//TODO: Apply Filtering ... .where(p => p....) ...
اینجا دقیقا جائی است که در صورت نیاز می‌توان کار فیلتر اطلاعات و اعمال متد where را انجام داد.

پس از مشخص شدن منبع داده و فیلتر آن در صورت نیاز، اکنون نوبت به مرتب سازی اطلاعات است:
var columnsMap = new Dictionary<string, Expression<Func<Product, object>>>()
            {
                ["productId"] = p => p.ProductId,
                ["productName"] = p => p.ProductName,
                ["isAvailable"] = p => p.IsAvailable,
                ["price"] = p => p.Price
            };
query = query.ApplyOrdering(queryModel, columnsMap);
توضیحات این مورد را پیشتر مطالعه کردید و هدف از آن، تهیه یک نگاشت ساده‌ی بین خواص رشته‌ای رسیده‌ی از سمت کلاینت به خواص مدل متناظر با آن است و سپس ارسال آن‌ها به همراه queryModel دریافتی از کاربر، برای اعمال مرتب سازی نهایی.

در آخر مطابق ساختار PagedQueryResult بازگشتی، ابتدا تعداد کل آیتم‌های منبع داده محاسبه شده‌است و سپس صفحه بندی به آن اعمال گردیده‌است. این ترتیب نیز مهم است و گرنه TotalItems دقیقا به همان تعداد ردیف‌های صفحه‌ی جاری محاسبه می‌شود:
var pagedResult = new PagedQueryResult<Product>();
pagedResult.TotalItems = query.Count();
query = query.ApplyPaging(queryModel);
pagedResult.Items = query.ToList();
return pagedResult;


در قسمت بعد، نحوه‌ی نمایش این اطلاعات را در سمت Angular بررسی خواهیم کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
مطالب
بررسی دو نکته (ترفند) کاربردی در SQL Server

1- اندازه گیری تعداد Transaction‌ها در واحد زمان روی یک Database خاص در SQL Server 

جهت بدست آوردن تعداد Transaction‌ها در واحد زمان( Transactions Per Second ) روی یک Database خاص در یک سیستم عملیاتی، جهت ارتقاء سخت افزاری ، تست فشار و ... می‌توانید از یک DMV با نام sys.dm_os_performance_counters به طریق زیر استفاده نمائید:
declare @cntr_value bigint

Select @cntr_value=cntr_value
from sys.dm_os_performance_counters
where instance_name='AdventureWorks' and
counter_name='Write Transactions/sec'

/* ایجاد یک تاخیر مثلاً یک ثانیه */
waitfor delay '00:00:01'

Select cntr_value -@cntr_value
from sys.dm_os_performance_counters
where instance_name='AdventureWorks' and
counter_name='Write Transactions/sec'
View معرفی شده تمامی شمارنده‌های عملکردی را برای یک Instance خاص شامل می‌شود، ستون instance_name  برابر نام بانک اطلاعاتی مورد نظر می‌باشد.

2- sys.sp_MSforeachtable 

از رویه‌های ذخیره شده UnDocumented در SQL Server می‌باشد و این قابلیت را دارا است که برای هر یک از جداول موجود در  یک بانک اطلاعاتی، یک رویه‌ای را اجرا کند. برای مثال با استفاده از دستور زیر، می‌توانید تعداد سطرها، اندازه‌ی داده‌ها و ایندکس‌های یک جدول را بدست آورید

EXEC sys.sp_MSforeachtable 'sp_spaceused ''?''';
به عنوان یک مثال کاربردی، با اجرای دستور زیر می‌توان جداول بانک اطلاعاتی مورد نظرتان را از لحاظ معیارهایی که پیشتر ذکر آن رفت، مورد بررسی قرار دهید.
 USE [AdventureWorksDW2008R2]
GO

CREATE TABLE #TableSpaceUsed(
[name] [nvarchar](120) NULL,
[rows] [nvarchar](120) NULL,
[reserved] [nvarchar](120) NULL,
[data] [nvarchar](120) NULL,
[index_size] [nvarchar](120) NULL,
[unused] [nvarchar](120) NULL
) ON [PRIMARY]

Insert Into #TableSpaceUsed
EXEC sys.sp_MSforeachtable 'sp_spaceused ''?''';

Select * from #TableSpaceUsed
Order by CAST([rows] as int) desc

Drop table #TableSpaceUsed
خروجی مثال فوق به شکل زیر است.


مطالب
درخت‌ها و گراف قسمت چهارم
در قسمت قبلی مبحث پیاده سازی ساختمان (ساختار) درخت‌های جستجوی دودویی را به پایان رساندیم. در این قسمت قرار است بر روی درخت متوازن بحث کنیم و آن را پیاده سازی نماییم.

درخت متوازن
همانطور که دیدید، عملیات جستجو  روی درخت جستجوی دو دویی به مراتب راحت و آسان‌تر است؛ ولی با این حال این درخت در عملیاتی چون درج و حذف، یک نقص فنی دارد و آن هم این است که نمی‌تواند عمق خود را کنترل کند و همینطور به سمت عمق‌های بیشتر و بزرگتر حرکت می‌کند. مثلا ساختار ترتیبی زیر را برای مقداری‌های 1 و 2 و 3 و 4 و  5و 6 در نظر بگیرید:

در این حالت دیگر درخت مانند یک درخت رفتار نمی‌کند و بیشتر شبیه لیست پیوندی است و عملیات جستجو همینطور کندتر و کندتر می‌شود و دیگر مثل سابق نخواهد بود، پیچیدگی برنامه بیشتر خواهد شد و از (Log(n به n می‌رسد. از آنجا که دوست داریم برای عملیات‌های رایجی چون درج و جستجو و حذف، همین پیچیدگی لگاریتمی را حفظ کنیم، از ساختاری جدیدتر بهره خواهیم برد.

درخت دودویی متوازن:  درختی است که در آن هیچ برگی، عمقش از هیچ برگی بیشتر نیست.

درخت دودویی متوازن کامل: درختی که تفاوتش در تعداد گره‌های چپ یا راست است و حداقل یک فرزند دارند.

درخت دودویی متوازن حتی اگر کامل هم نباشد، در عملیات پایه‌ای چون افزودن، حذف و جستجو در بدترین حالت هم با پیچیدگی لگاریتمی تعداد گام‌ها همراه است. برای اینکه این درخت با به هم ریختگی توازنش روبرو نشود، باید حین انجام عملیات پایه، جایگاه تعداد المان‌های آن بررسی و اصلاح شود که به این عملیات چرخش یا دوران Rotation می‌گویند. انجام این عملیات بستگی دارد که پیاده سازی این درخت به چه شکلی باشد و به چه صورتی پیاده سازی شده باشد. از پیاده سازی‌های این درخت می‌توان به درخت سرخ-سیاه Black Red Tree ، ای وی ال AVL Tree ، اسپلی Splay و ... اشاره کرد.

با توجه به موارد بالا میتوانیم به نتایج زیر برسیم که چرا این درخت در هر حالت، پیچیدگی زمانی خودش را در لگاریتم n حفظ می‌کند:

  • المان‌ها و عناصرش را مرتب شده نگه می‌دارد.
  • خودش را متوازن نگه داشته و اجازه نمی‌دهد عمقش بیشتر از لگاریتم n شود.

نمونه ای از درخت جستجوی دو دویی

همان درخت ولی به صورت متوازن با پیاده سازی AVL


درخت‌های متوازن هم می‌متوانند دو دویی یا باینری باشند و هم غیر باینری non-Binary

درخت‌های دو دویی در انجام عملیات بسیار سریع هستند و در بالا به تعدادی از انواع پیاده سازی‌های آن اشاره کردیم.

درخت‌های غیر باینری نیز مرتب و متوازن بوده، ولی می‌توانند بیش از یک کلید داشته باشند و همچنین بیشتر از دو فرزند. این درخت‌ها نیز عملیات خود را بسیار سریع انجام می‌دهند. از نمونه‌های این درخت می‌توان به B Tree,B+ Tree,Interval Tree اشاره کرد.

از آنجا که پیاده‌سازی این نوع درخت کمی دشوار و پیچیده و طولانی است و همچنین پیاده سازی‌های مختلفی دارد؛ تعدادی از منابع موجود را در زیر معرفی می‌کنیم:

در خود دات نت در فضای نام system.collection.generic کلاس TreeSet یک نوع پیاده سازی از این درخت است که این پیاده سازی از نوع درخت سرخ سیاه می‌باشد و جالب است بدانید، در طی بیست گام می‌تواند در یک میلیون آیتم به جستجو بپردازد ولی خبر بد اینکه استفاده مستقیم از این کلاس ممکن نیست چرا که این کلاس به صورت داخلی internal برای استفاده‌ی خود کتابخانه طراحی شده است ولی خبر خوب اینکه کلاس sortedDictionary از این کلاس بهره برده است و به صورت عمومی در دسترس ما قرار گرفته است. همچنین کلاس SortedSet هم از دات نت 4  نیز در دسترس است.

 

کتابخانه‌های خارجی جهت استفاده در دات نت که به پیاده‌سازی درخت‌های متوازن پرداخته‌اند:

پیاده سازی Splay Tree با سی شارپ 

پیاده سازی AVL به صورت بهینه و آسان برای استفاده

پیاده سازی درخت AVL با کارایی بالا

پیاده سازی درخت AVL به همراه نمایش گرافیکی آن

درخت سرخ-سیاه

کتابخانه ای متن باز برای درخت سرخ-سیاه

درخت متوازن به همراه جست و جو و حذف و پیمایش ها

غیر دودویی‌ها

پیاده سازی B Tree

پیاده سازی Interval Tree 
پیاده سازی Interval Tree در سی ++  
مطالب
نحوه‌ی مشاهده‌ی خروجی SQL تولید شده توسط WCF RIA Services

این روزها با وجود ORMs ، کوئری SQL‌ نوشتن شبیه به دورانی شده که با وجود زبان‌های سطح بالا، عده‌ای علاقمند هستند با استفاده از زبان اسمبلی برنامه نویسی کنند! WCF RIA Services به صورت پیش فرض از entity framework استفاده می‌کند (هر چند می‌توان از سایر ORMs هم استفاده کرد)، بنابراین عنوان صحیح‌تر بحث این خواهد بود: چگونه خروجی SQL تولید شده توسط Entity framework را بررسی کنیم؟

الف) استفاده از SQL Server profiler
اولین برنامه‌ای که از سال‌ها قبل، حتی پیش از ظهور ORMs وجود داشته، برنامه‌ی SQL server profiler است، که عموما در مسیر ذیل قابل دستیابی است:
Start Menu->Programs->Microsoft SQL Server 2008->Performance Tools->SQL Server profiler



نکته مهم:
حین کار با SQL Server profiler ، ممکن است انبوهی از کوئری‌های دیگر مثلا مرتبط با SQL Server agent یا reporting services و غیره نیز لاگ شوند. اما الان ما تنها به کوئری‌های برنامه‌ی خود نیاز داریم. برای این منظور به کانکشن استرینگ خود، گزینه‌ی Application Name=My Application Name را نیز اضافه کنید:

<connectionStrings>
<add name="dmEntities" connectionString="metadata=res://*/Models.dmDataModel.csdl|res://*/Models.dmDataModel.ssdl|res://*/Models.dmDataModel.msl;provider=System.Data.SqlClient;provider connection string=&quot;Data Source=(local);Initial Catalog=dm;Integrated Security=True;Application Name=My Application Name;MultipleActiveResultSets=True&quot;" providerName="System.Data.EntityClient" />
</connectionStrings>

اکنون اگر برنامه را با پروفایلر مورد بررسی قرار دهید خروجی به صورت زیر خواهد بود:



برای فیلتر کردن Application Name مورد نظر، در ابتدای کار که یک سشن جدید را آغاز می‌کنید به برگه‌ی events selection مراجعه کرده و بر روی دکمه‌ی column filter کلیک کنید. گزینه‌ی application name را در صفحه‌ی باز شده انتخاب نموده و در قسمت Like آن مطابق تصویر زیر ، نام برنامه‌ی خود را وارد نمائید:




ب) استفاده از IntelliTrace در VS.NET 2010
برنامه را در حالت دیباگ در VS.NET 2010 اجرا کنید. در هر لحظه‌ای می‌توان روی گزینه‌ی Break all کلیک کرد و خروجی SQL تولید شده را نیز علاوه بر اطلاعات دیگر مشاهده نمود:




ج) استفاده از برنامه‌ی حرفه‌ای entity framework profiler
این برنامه از هر دو مورد قبل کاملتر بوده و اساسا برای لاگ کردن کوئری‌ها، مدت زمان اجرا، گزارشگیری از وضعیت برنامه، کدامیک از کوئری‌ها سنگین‌تر هستند، حتی از طریق کدام متد فراخوانی شده‌اند، ارائه‌ی گزارشات و راهنمایی‌هایی در مورد چگونگی بهبود کارآیی برنامه‌ی تهیه شده و امثال آن کاربرد دارد.



استفاده از آن هم بسیار ساده است. ابتدا ارجاعی را به اسمبلی HibernatingRhinos.Profiler.Appender.v4.0 به پروژه‌ی ASP.NET خود اضافه کنید (همان پروژه‌ی هوست مربوط به WCF RIA Service ما). سپس به فایل Global.asax.cs برنامه مراجعه کرده و یک سطر ذیل را اضافه کنید:

protected void Application_Start(object sender, EventArgs e)
{
HibernatingRhinos.Profiler.Appender.EntityFramework.EntityFrameworkProfiler.Initialize();
}

از این پس تنها کافی است برنامه‌ی پروفایلر در حال اجرا بوده و برنامه شما نیز اجرا شود. کلیه‌ی تبادلات با دیتابیس لاگ خواهند شد.

مطالب
نحوه ایجاد Sequence و استفاده آن در Sql Server 2012
قبل از ایجاد Sequence در Sql Server 2012، توضیح مختصری را درباره آن می‌دهم.
در واقع Sequence روشی برای تولید اعداد ترتیبی با قابلیت افزایش یا کاهش عدد‌های دلخواه می‌باشد که توسط کاربر یا برنامه نویس ایجاد می‌شود. بنابراین Sequenceها User-Defined می‌باشند.
در اینجا ممکن است سئوالی پیش بیاید که اینکار توسط Identity هم قابل انجام است، اما چرا استفاده از Sequence توسط مایکروسافت پیشنهاد می‌شود.
بدلایل زیر استفاده از Sequence بهتر می‌باشد:
  1. ممکن است Application شما قبل از درج رکورد، درون یک جدول نیاز به عدد منحصربفردی داشته باشد.
  2. عدد تولید شده بوسیله Sequence را می‌توانید بین جداول یا ستونهای مختلف یک جدول به اشتراک بگذارید.
  3. می‌توانید روند تولید اعداد ترتیبی را Restart نمایید. به عبارت دیگر قابلیت Restart نمودن Sequence وجود دارد.
  4. می‌توانید Sequence خود را براساس Sort یک یا چند فیلد، تنظیم نمایید.

Syntax آن به شرح ذیل می‌باشد:
CREATE SEQUENCE [schema_name . ] sequence_name
    [ AS [ built_in_integer_type | user-defined_integer_type ] ]
    [ START WITH <constant> ]
    [ INCREMENT BY <constant> ]
    [ { MINVALUE [ <constant> ] } | { NO MINVALUE } ]
    [ { MAXVALUE [ <constant> ] } | { NO MAXVALUE } ]
    [ CYCLE | { NO CYCLE } ]
    [ { CACHE [ <constant> ] } | { NO CACHE } ]
    [ ; ]

شرح Syntax :
  • در زمان ایجاد Sequence، نوع آن می‌بایست عددی باشد، چنانچه ،Type آن را مشخص ننمایید، SQL Server، نوع آن را bigint در نظر می‌گیرد.
  • Start With: بدین مفهوم می‌باشد، که Sequence ایجاد شده از چه عددی آغاز شود.
  • INCREMENT BY: مفهومش این است که Sequence به چه مقداری افزایش یا کاهش یابد. به عبارت دیگری عدد تولید شده براساس مقدار Increment by تولید می‌شود.
  • Minvalue: کمترین مقداری که Sequence می‌تواند ایجاد نماید.
  • Maxvalue :بیشترین مقداری که Sequence می‌تواند ایجاد نماید.
  • Cycle :مقداری را که برای Cycle تعیین می‌نماییم، بدین مفهوم است که Sequence پس از چه عددی می‌بایست Restart شود.
  • Cache :عددی که برای Cache در نظر می‌گیریم، مفهومش این است که چه تعداد از اعداد تولید شده توسط Sequence، قبل از استفاده، می‌تواند در Cache قرار گیرد.

در ادامه با یک مثال ساده، یک Sequence ایجاد می‌نماییم:
CREATE SEQUENCE [dbo].[SequenceTest] 
 AS [int]
 START WITH 1
 INCREMENT BY 1
 MINVALUE 1
 MAXVALUE 30
 CYCLE 
 CACHE 
GO
در مثال بالا Start with برابر یک است، یعنی اولین عددی که تولید می‌شود، برابر یک است،INCREMENT BY برابر یک است یعنی در هر بار فراخوانی Sequence یک عدد به عدد تولید شده قبلی افزوده می‌شود. مقدار Minvalue برابر یک است، یعنی کمترین مقداری که Sequence می‌تواند تولید نماید برابر یک است. مقدار Maxvalue برابر 30 است، یعنی بیشترین مقداری که Sequence می‌تواند تولید نماید برابر 30 می‌باشد. و Cycle هم برای Sequence فوق در نظر گرفته شده است، بدین مفهوم ، که Sequence به مقدار 30 برسد، Restart شده و از مقدار یک شروع به تولید اعداد می‌نماید.
برای اینکه بتوانیم مقدار Sequence را بدست آوریم، کافیست از Syntax زیر استفاده نمایید:
NEXT VALUE FOR [ database_name . ] [ schema_name . ]  sequence_name
   [ OVER (<over_order_by_clause>) ]
به عنوان مثال داریم:


اگر Select بالا را تا 30 بار انجام دهید، برای دفعه 31 مقدار آن یک می‌شود، چون در زمان تعریف Cycle ،Sequence  را انتخاب کرده بودیم. در غیر اینصورت برای دفعه 31 با خطا زیر مواجه می‌شوید.
Msg 11728, Level 16, State 1, Line 1
The sequence object 'SequenceTest' has reached its minimum or maximum value. Restart the sequence object to allow new values to be generated.
 یکی از امکانات جالب Sequence  این است که شما می‌توانید Sequence را روی یک فیلد Sort شده تنظیم نمایید، برای روش شدن مطلب به مثال زیر توجه نمایید:
در ابتدا مطابق Script زیر جدولی را ایجاد و مقادیری را درون آن درج می‌نماییم:
create table Kids
( ID int,
Name varchar(50)
);
Go
insert Kids
 values
  (1,'Emma')
, (1,'Tabitha')
, (2,'Kendall')
, (3,'Delaney')
, (4,'Kyle')
, (5,'Jessica')
, (6,'Josh')
, (7,'Kirsten')
, (8,'Amanda')
, (9,'Jimmy')
;
سپس یک Schema به نام Sample ایجاد می‌کنیم:
CREATE SCHEMA Samples ;
GO
در ادامه یک Sequence به نام Test ایجاد می‌کنیم:
CREATE SEQUENCE Samples.Test
    AS tinyint
    START WITH 1
    INCREMENT BY 1 ;
GO
حال Query زیر را اجرا می‌نماییم:
SELECT NEXT VALUE FOR Samples.Test OVER (ORDER BY Name) AS NutID, ID, Name FROM  test1.Kids
WHERE Name LIKE '%e%' ;
در Query بالا روی فیلد Name به صورت صعودی عملیات Sort انجام می‌شود، و سپس Sequence روی آن اعمال می‌گردد، برای روشن‌تر شدن مطلب خروجی را مشاهده نمایید که تعداد رکورد آن 6 میباشد.


امیدوارم مطلب فوق مفید واقع شده باشد.
مطالب
مدیریت Join در NHibernate 3.0

مباحث eager fetching/loading (واکشی حریصانه) و lazy loading/fetching (واکشی در صورت نیاز، با تاخیر، تنبل) جزو نکات کلیدی کار با ORM های پیشرفته بوده و در صورت عدم اطلاع از آن‌ها و یا استفاده‌ی ناصحیح از هر کدام، باید منتظر از کار افتادن زود هنگام سیستم در زیر بار چند کاربر همزمان بود. به همین جهت تصور اینکه "با استفاده از ORMs دیگر از فراگیری SQL راحت شدیم!" یا اینکه "به من چه که پشت صحنه چه اتفاقی می‌افته!" بسی مهلک و نادرست است!
در ادامه به تفصیل به این موضوع پرداخته خواهد شد.

ابزار مورد نیاز

در این مطلب از برنامه‌ی NHProf استفاده خواهد شد.
اگر مطالب NHibernate این سایت را دنبال کرده باشید، در مورد لاگ کردن SQL تولیدی به اندازه‌ی کافی توضیح داده شده یا حتی یک ماژول جمع و جور هم برای مصارف دم دستی نوشته شده است. این موارد شاید این ایده را به همراه داشته باشند که چقدر خوب می‌شد یک برنامه‌ی جامع‌تر برای این نوع بررسی‌ها تهیه می‌شد. حداقل SQL نهایی فرمت می‌شد (یعنی برنامه باید مجهز به یک SQL Parser تمام عیار باشد که کار چند ماهی هست ...؛ با توجه به اینکه مثلا NHibernate از افزونه‌های SQL ویژه بانک‌های اطلاعاتی مختلف هم پشتیبانی می‌کند، مثلا T-SQL مایکروسافت با یک سری ریزه کاری‌های منحصر به MySQL متفاوت است)، یا پس از فرمت شدن، syntax highlighting به آن اضافه می‌شد، در ادامه مشخص می‌کرد کدام کوئری‌ها سنگین‌تر هستند، کدامیک نشانه‌ی عدم استفاده‌ی صحیح از ORM مورد استفاده است، چه مشکلی دارد و از این موارد.
خوشبختانه این ایده‌ها یا آرزوها با برنامه‌ی NHProf محقق شده است. این برنامه برای استفاده‌ی یک ماه اول آن رایگان است (آدرس ایمیل خود را وارد کنید تا یک فایل مجوز رایگان یک ماهه برای شما ارسال گردد) و پس از یک ماه، باید حداقل 300 دلار هزینه کنید.


واکشی حریصانه و غیرحریصانه چیست؟

رفتار یک ORM جهت تعیین اینکه آیا نیاز است برای دریافت اطلاعات بین جداول Join صورت گیرد یا خیر، واکشی حریصانه و غیرحریصانه را مشخص می‌سازد.
در حالت واکشی حریصانه به ORM خواهیم گفت که لطفا جهت دریافت اطلاعات فیلدهای جداول مختلف، از همان ابتدای کار در پشت صحنه، Join های لازم را تدارک ببین. در حالت واکشی غیرحریصانه به ORM خواهیم گفت به هیچ عنوان حق نداری Join ایی را تشکیل دهی. هر زمانی که نیاز به اطلاعات فیلدی از جدولی دیگر بود باید به صورت مستقیم به آن مراجعه کرده و آن مقدار را دریافت کنی.
به صورت خلاصه برنامه نویس در حین کار با ORM های پیشرفته نیازی نیست Join بنویسد. تنها باید ORM را طوری تنظیم کند که آیا اینکار را حتما خودش در پشت صحنه انجام دهد (واکشی حریصانه)، یا اینکه خیر، به هیچ عنوان SQL های تولیدی در پشت صحنه نباید حاوی Join باشند (lazy loading).


چگونه واکشی حریصانه و غیرحریصانه را در NHibernate 3.0 تنظیم کنیم؟

در NHibernate اگر تنظیم خاصی را تدارک ندیده و خواص جداول خود را به صورت virtual معرفی کرده باشید، تنظیم پیش فرض دریافت اطلاعات همان lazy loading است. به مثالی در این زمینه توجه بفرمائید:

مدل برنامه:
مدل برنامه همان مثال کلاسیک مشتری و سفارشات او می‌باشد. هر مشتری چندین سفارش می‌تواند داشته باشد. هر سفارش به یک مشتری وابسته است. هر سفارش نیز از چندین قلم جنس تشکیل شده است. در این خرید، هر جنس نیز به یک سفارش وابسته است.


using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Customer
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual IList<Order> Orders { get; set; }
}
}

using System;
using System.Collections.Generic;
namespace CustomerOrdersSample.Domain
{
public class Order
{
public virtual int Id { get; set; }
public virtual DateTime OrderDate { set; get; }
public virtual Customer Customer { get; set; }
public virtual IList<OrderItem> OrderItems { set; get; }
}
}

namespace CustomerOrdersSample.Domain
{
public class OrderItem
{
public virtual int Id { get; set; }
public virtual Product Product { get; set; }
public virtual int Quntity { get; set; }
public virtual Order Order { set; get; }
}
}

namespace CustomerOrdersSample.Domain
{
public class Product
{
public virtual int Id { set; get; }
public virtual string Name { get; set; }
public virtual decimal UnitPrice { get; set; }
}
}

که جداول متناظر با آن به صورت زیر خواهند بود:
    create table Customers (
CustomerId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
primary key (CustomerId)
)

create table Orders (
OrderId INT IDENTITY NOT NULL,
OrderDate DATETIME null,
CustomerId INT null,
primary key (OrderId)
)

create table OrderItems (
OrderItemId INT IDENTITY NOT NULL,
Quntity INT null,
ProductId INT null,
OrderId INT null,
primary key (OrderItemId)
)

create table Products (
ProductId INT IDENTITY NOT NULL,
Name NVARCHAR(255) null,
UnitPrice NUMERIC(19,5) null,
primary key (ProductId)
)

alter table Orders
add constraint fk_Customer_Order
foreign key (CustomerId)
references Customers

alter table OrderItems
add constraint fk_Product_OrderItem
foreign key (ProductId)
references Products

alter table OrderItems
add constraint fk_Order_OrderItem
foreign key (OrderId)
references Orders

همچنین یک سری اطلاعات آزمایشی زیر را هم در نظر بگیرید: (بانک اطلاعاتی انتخاب شده SQL CE است)

SET IDENTITY_INSERT [Customers] ON;
GO
INSERT INTO [Customers] ([CustomerId],[Name]) VALUES (1,N'Customer1');
GO
SET IDENTITY_INSERT [Customers] OFF;
GO
SET IDENTITY_INSERT [Products] ON;
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (1,N'Product1',1000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (2,N'Product2',2000.00000);
GO
INSERT INTO [Products] ([ProductId],[Name],[UnitPrice]) VALUES (3,N'Product3',3000.00000);
GO
SET IDENTITY_INSERT [Products] OFF;
GO
SET IDENTITY_INSERT [Orders] ON;
GO
INSERT INTO [Orders] ([OrderId],[OrderDate],[CustomerId]) VALUES (1,{ts '2011-01-07 11:25:20.000'},1);
GO
SET IDENTITY_INSERT [Orders] OFF;
GO
SET IDENTITY_INSERT [OrderItems] ON;
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (1,10,1,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (2,5,2,1);
GO
INSERT INTO [OrderItems] ([OrderItemId],[Quntity],[ProductId],[OrderId]) VALUES (3,20,3,1);
GO
SET IDENTITY_INSERT [OrderItems] OFF;
GO

دریافت اطلاعات :
می‌خواهیم نام کلیه محصولات خریداری شده توسط مشتری‌ها را به همراه نام مشتری و زمان خرید مربوطه، نمایش دهیم (دریافت اطلاعات از 4 جدول بدون join نویسی):

var list = session.QueryOver<Customer>().List();

foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}

خروجی به صورت زیر خواهد بود:
Customer1:2011/01/07 11:25:20 :Product1
Customer1:2011/01/07 11:25:20 :Product2
Customer1:2011/01/07 11:25:20 :Product3
اما بهتر است نگاهی هم به پشت صحنه عملیات داشته باشیم:



همانطور که مشاهده می‌کنید در اینجا اطلاعات از 4 جدول مختلف دریافت می‌شوند اما ما Join ایی را ننوشته‌ایم. ORM هرجایی که به اطلاعات فیلدهای جداول دیگر نیاز داشته، به صورت مستقیم به آن جدول مراجعه کرده و یک کوئری، حاصل این عملیات خواهد بود (مطابق تصویر جمعا 6 کوئری در پشت صحنه برای نمایش سه سطر خروجی فوق اجرا شده است).
این حالت فقط و فقط با تعداد رکورد کم بهینه است (و به همین دلیل هم تدارک دیده شده است). بنابراین اگر برای مثال قصد نمایش اطلاعات حاصل از 4 جدول فوق را در یک گرید داشته باشیم، بسته به تعداد رکوردها و تعداد کاربران همزمان برنامه (خصوصا در برنامه‌های تحت وب)، بانک اطلاعاتی باید بتواند هزاران هزار کوئری رسیده حاصل از lazy loading را پردازش کند و این یعنی مصرف بیش از حد منابع (IO بالا، مصرف حافظه بالا) به همراه بالا رفتن CPU usage و از کار افتادن زود هنگام سیستم.
کسانی که پیش از این با SQL نویسی خو گرفته‌اند احتمالا الان منابع موجود را در مورد نحوه‌ی نوشتن Join در NHibernate زیر و رو خواهند کرد؛ زیرا پیش از این آموخته‌اند که برای دریافت اطلاعات از دو یا چند جدول مرتبط باید Join نوشت. اما همانطور که پیشتر نیز عنوان شد، اگر با جزئیات کار با NHibernate آشنا شویم، نیازی به Join نویسی نخواهیم داشت. اینکار را خود ORM در پشت صحنه باید و می‌تواند مدیریت کند. اما چگونه؟
در NHibernate 3.0 با معرفی QueryOver که جایگزینی از نوع strongly typed همان ICriteria API قدیمی است، یا با معرفی Query که همان LINQ to NHibernate می‌باشد، متدی به نام Fetch نیز تدارک دیده شده است که استراتژی‌های lazy loading و eager loading را به سادگی توسط آن می‌توان مشخص نمود.

مثال: دریافت اطلاعات با استفاده از QueryOver

var list = session
.QueryOver<Customer>()
.Fetch(c => c.Orders).Eager
.Fetch(c => c.Orders.First().OrderItems).Eager
.Fetch(c => c.Orders.First().OrderItems.First().Product).Eager
.List();

foreach (var customer in list)
{
foreach (var order in customer.Orders)
{
foreach (var orderItem in order.OrderItems)
{
Console.WriteLine("{0}:{1}:{2}", customer.Name, order.OrderDate, orderItem.Product.Name);
}
}
}

پشت صحنه:



اینبار فقط یک کوئری حاصل عملیات بوده و join ها به صورت خودکار با توجه به متدهای Fetch ذکر شده که حالت eager loading آن‌ها صریحا مشخص شده است، تشکیل شده‌اند (6 بار رفت و برگشت به بانک اطلاعاتی به یکبار تقلیل یافت).

نکته 1: نتایج تکراری
اگر حاصل join آخر را نمایش دهیم، نتایجی تکراری خواهیم داشت که مربوط است به مقدار دهی customer با سه وهله از شیء مربوطه تا بتواند واکشی حریصانه‌ی مجموعه اشیاء فرزند آن‌را نیز پوشش دهد. برای رفع این مشکل یک سطر TransformUsing باید اضافه شود:
...
.TransformUsing(NHibernate.Transform.Transformers.DistinctRootEntity)
.List();


دریافت اطلاعات با استفاده از LINQ to NHibernate3.0
برای اینکه بتوان متدهای Fetch ذکر شده را به LINQ to NHibernate 3.0 اعمال نمود، ذکر فضای نام NHibernate.Linq ضروری است. پس از آن خواهیم داشت:
var list = session
.Query()
.FetchMany(c => c.Orders)
.ThenFetchMany(o => o.OrderItems)
.ThenFetch(p => p.Product)
.ToList();

اینبار از FetchMany، سپس ThenFetchMany (برای واکشی حریصانه مجموعه‌های فرزند) و در آخر از ThenFetch استفاده خواهد شد.

همانطور که ملاحظه می‌کنید حاصل این کوئری، با کوئری قبلی ذکر شده یکسان است. هر دو، اطلاعات مورد نیاز از دو جدول مختلف را نمایش می‌دهند. اما یکی در پشت صحنه شامل چندین و چند کوئری برای دریافت اطلاعات است، اما دیگری تنها از یک کوئری Join دار تشکیل شده است.


نکته 2: خطاهای ممکن
ممکن است حین تعریف متدهای Fetch در زمان اجرا به خطاهای Antlr.Runtime.MismatchedTreeNodeException و یا Specified method is not supported و یا موارد مشابهی برخورد نمائید. تنها کاری که باید انجام داد جابجا کردن مکان بکارگیری extension methods است. برای مثال متد Fetch باید پس از Where در حالت استفاده از LINQ ذکر شود و نه قبل از آن.

مطالب
تهیه گزارشات Crosstab به کمک LINQ

در گزارشات Crosstab، ردیف‌های یک گزارش، تبدیل به ستون‌های آن می‌شوند؛ به همین جهت به آن‌ها Pivot tables هم می‌گویند.
برای مثال فرض کنید که قصد دارید گزارش تعداد ساعت کارکرد را به ازای هر پروژه در طول چند ماه تعیین کنید. گزارش متداول از این نوع اطلاعات، یک لیست بلند بالای بی‌مفهوم است. این گزارش تشکیل شده از صدها رکورد به ازای کارکنان مختلف در پروژه‌های مختلف و ... هیچ ارزش آماری خاصی ندارد. یک گزارش بدوی است. زمانیکه این گزارش را تبدیل به حالت crosstab می‌کنیم، اولین ستون فقط یک شماره پروژه خواهد بود و ستون‌های بعدی، مثلا نام ماه‌ها و مقادیر آن‌ها هم جمع کارکرد افراد بر روی یک پروژه مشخص.

مثال اول) تهیه گزارش Crosstab جمع هزینه‌های واحدهای مختلف به تفکیک ماه

کلاس هزینه‌های زیر را در نظر بگیرید که به کمک آن می‌توان به ازای هر واحد یا دپارتمان در تاریخ‌های متفاوت، هزینه‌ای را مشخص ساخت:

using System;

namespace Pivot.Sample1
{
public class Expense
{
public DateTime Date { set; get; }
public string Department { set; get; }
public decimal Expenses { set; get; }
}
}

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

using System;
using System.Collections.Generic;

namespace Pivot.Sample1
{
public class ExpenseDataSource
{
public static IList<Expense> ExpensesDataSource()
{
return new List<Expense>
{
new Expense { Date = new DateTime(2011,11,1), Department = "Computer", Expenses = 100 },
new Expense { Date = new DateTime(2011,11,1), Department = "Math", Expenses = 200 },
new Expense { Date = new DateTime(2011,11,1), Department = "Physics", Expenses = 150 },

new Expense { Date = new DateTime(2011,10,1), Department = "Computer", Expenses = 75 },
new Expense { Date = new DateTime(2011,10,1), Department = "Math", Expenses = 150 },
new Expense { Date = new DateTime(2011,10,1), Department = "Physics", Expenses = 130 },

new Expense { Date = new DateTime(2011,9,1), Department = "Computer", Expenses = 90 },
new Expense { Date = new DateTime(2011,9,1), Department = "Math", Expenses = 95 },
new Expense { Date = new DateTime(2011,9,1), Department = "Physics", Expenses = 100 }
};
}
}
}

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


که ... خروجی مطلوبی نیست. در اینجا ما فقط 9 رکورد داریم؛ اما در عمل به ازای هر روز، یک رکورد می‌تواند وجود داشته باشد و این لیست طولانی، هیچ ارزش آماری خاصی ندارد. می‌خواهیم سرستون‌های گزارش ما مطابق جدول زیر باشند:


یعنی اگر سه ماه را در نظر بگیریم با هر تعداد رکورد، فقط سه ردیف به ازای هر ماه باید حاصل شود و ستون‌های دیگر هم نام بخش‌ها یا واحدهای موجود باشند.
برای رسیدن به این خروجی Crosstab، می‌توان کوئری LINQ زیر را به کمک امکانات گروه بندی اطلاعات آن تهیه کرد:

using System.Collections;
using System.Linq;

namespace Pivot.Sample1
{
public class PivotTable
{
public static IList ExpensesCrossTab()
{
return ExpenseDataSource
.ExpensesDataSource()
.GroupBy(t =>
new
{
Year = t.Date.Year,
Month = t.Date.Month
})
.Select(myGroup =>
new
{
//Year = myGroup.Key.Year,
Month = myGroup.Key.Month,
ComputerDepartment = myGroup.Where(x => x.Department == "Computer").Sum(x => x.Expenses),
MathDepartment = myGroup.Where(x => x.Department == "Math").Sum(x => x.Expenses),
PhysicsDepartment = myGroup.Where(x => x.Department == "Physics").Sum(x => x.Expenses)
})
.ToList();
}
}
}

که اینبار خروجی زیر را تولید می‌کند.


اگر علاقمند باشید که مثال فوق را در برنامه‌ی LINQPad آزمایش کنید، این فایل را دریافت نموده و در آن برنامه باز نمائید.


مثال دوم) تهیه لیست Crosstab حضور و غیاب افراد در طول یک هفته

کلاس StudentStat را جهت ثبت اطلاعات حضور یک دانشجو، می‌توان به شکل زیر تعریف کرد:

using System;

namespace Pivot.Sample2
{
public class StudentStat
{
public int Id { set; get; }
public string Name { set; get; }
public DateTime Date { set; get; }
public bool IsPresent { set; get; }
}
}

و بر همین اساس یک منبع داده فرضی جهت انجام گزارشات می‌تواند به نحو زیر تهیه شود:

using System;
using System.Collections.Generic;

namespace Pivot.Sample2
{
public class StudentsStatDataSource
{
public static IList<StudentStat> CreateMonthlyReportDataSource()
{
var result = new List<StudentStat>();
var rnd = new Random();

for (int day = 1; day < 6; day++)
{
for (int student = 1; student < 6; student++)
{
result.Add(new StudentStat
{
Id = student,
Date = new DateTime(2011, 11, day),
IsPresent = rnd.Next(-1, 1) == 0 ? true : false,
Name = "student " + student
});
}
}

return result;
}
}
}

خروجی این گزارش هم در این حالت ساده با 5 دانشجو و فقط 5 روز، 25 رکورد خواهد بود:


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


برای رسیدن به این خروجی Crosstab، مثلا می‌توان از کوئری LINQ زیر کمک گرفت که بر اساس شماره دانشجویی اطلاعات را گروه بندی کرده است:

using System.Collections;
using System.Linq;

namespace Pivot.Sample2
{
public class PivotTable
{
public static IList StudentsStatCrossTab()
{
return StudentsStatDataSource
.CreateWeeklyReportDataSource()
.GroupBy(x =>
new
{
x.Id
})
.Select(myGroup =>
new
{
myGroup.Key.Id,
Name = myGroup.First().Name,
Day1IsPresent = myGroup.Where(x => x.Date.Day == 1).First().IsPresent,
Day2IsPresent = myGroup.Where(x => x.Date.Day == 2).First().IsPresent,
Day3IsPresent = myGroup.Where(x => x.Date.Day == 3).First().IsPresent,
Day4IsPresent = myGroup.Where(x => x.Date.Day == 4).First().IsPresent,
Day5IsPresent = myGroup.Where(x => x.Date.Day == 5).First().IsPresent,
PresentsCount = myGroup.Where(x => x.IsPresent).Count(),
AbsentsCount = myGroup.Where(x => !x.IsPresent).Count()
})
.ToList();
}
}
}

و این کوئری خروجی زیر را تولید می‌کند که از هر لحاظ نسبت به لیست قبلی مفهوم‌تر است:


فایل LINQPad این مثال را می‌توانید از اینجا دریافت کنید.

نظرات مطالب
وضعیت فناوری‌های مرتبط با دات نت از دیدگاه مرگ و زندگی!
همین الان سایت stack overflow مبتنی بر LINQ to SQL و ASP.NET MVC است (مطابق آخرین آماری که منتشر کرده‌اند) و تعداد تراکنش‌های بسیار بالایی هم دارد. بنابراین، بله. عاقلانه است و دارد استفاده می‌شود : (+)