مطالب
آموزش MDX Query - قسمت ششم – شروع کار با دستورات MDX

امروز اولین دستورات MDX را خواهیم نوشت قبل از شروع کار فراموش نکنید موارد زیر را حتما انجام داده باشید :

  1. نصب پایگاه داده ی Adventure Work DW 2008 و همچنین نصب پایگاه داده‌ی چند بعدی  Adventure Work DW 2008  روی SSAS
  2. مطاله قسمت‌های قبلی برای آشنایی با مفاهیم پایه .

در صورتیکه پیش شرایط فوق را نداشته باشید، احتمالا در ادامه با مشکلاتی مواجه خواهید شد؛ زیرا برای آموزش MDX Query ها از پایگاه داده‌ی Adventure Work DW 2008 استفاده شده است. 

دقت داشته باشید که MDX Query ‌ها تا حدودی شبیه T/SQL  می‌باشند؛ اما مطلقا از نظر مفهومی با هم شباهت ندارند. به عبارت دیگر ما در T/SQL  با یک مدل رابطه‌ای سرو کار داریم در حالیکه در MDX ‌ها با یک پایگاه داده چند بعدی کار می‌کنیم. به بیان دیگر در پایگاه داده‌های رابطه‌ای صحبت از جداول، ردیف‌ها، ستون‌ها و ضرب دکارتی مجموعه‌ها می‌باشد، اما در پایگاه داده‌های چند بعدی در خصوص Dimension,Fact,Cube,Tuple و ... صحبت می‌کنیم. البته ماکروسافت تلاش کرده‌است تا حد زیادی Syntax ‌ها شبیه به یکدیگر باشند.

نحوه‌ی نوشتن یک Select در MDX ‌ها به صورت زیر می‌باشد :

Select
{} On Columns ,
{} On Rows
From <Cube_Name>
Where <Condition>

در ادامه با اجرای هر کوئری، توضیحات لازم در خصوص آن ارایه می‌گردد و با پیگیری این آموزش‌ها می‌توانید مفاهیم، توابع و ... را در MDX Query ‌ها بیاموزید.

برای اجرای دستورات زیر باید Microsoft SQL Server Management Studio را باز نمایید و به سرویس SSAS متصل شوید. سپس پایگاه داده‌ی Adventure Works DW 2008R2 را انتخاب نمایید و از Cubes Adventure Works را انتخاب نمایید.

حال دکمه‌ی New Query را در بالای صفحه بزنید ( Ctrl + N )  

سپس در صفحه‌ی باز شده می‌توانید Cube یا SubCube ‌های آن Cube را انتخاب کرده و کمی پایین‌تر Measure Group را خواهیم داشت و در انتها Measure ‌ها و Dimension ‌ها قرار گرفته‌اند. (در هنگام نوشتن Select می‌توان از عمل Drag&Drop برای آسان‌تر شدن نوشتن MDX Query ‌ها نیز استفاده کنید)

متاسفانه هنوز در IDE مربوط به SQL Server کلیدی برای مرتب سازی دستورات MDX وجود ندارد و البته در نرم افزار هایی مانند SQL Toll Belt هم چنین چیزی قرار داده نشده است . بنابر این توصیه می‌شود در نوشتن دستورات MDX تمام تلاش خود را بکنید تا دستوراتی مرتب و خوانا را تولید کنید.

با اجرای دستور زیر اولین کوئری خود را در پایگاه داده‌ی چند بعدی بنویسید (برای اجرا کلید F5 مانند T/SQL کار خواهد کرد.)

Select
From [Adventure Works]

شاید تعجب کنید. کوئری فاقد قسمت Projection می‌باشد! در MDX ‌ها می‌توان هیچ سطر یا ستونی را انتخاب نکرد. اما چگونه؟ و خروجی نمایش داده شده چیست؟

برای توضیح مطلب فوق باید در خصوص Default Measure کمی اطلاعات داشته باشید. در هنگام Deploy کردن پروژه در SSAS برای هر Cube یک Measure به عنوان Measure پیش فرض انتخاب شده. بنابر این در صورتیکه هیچ گونه Projection یا Where ایی اعمال نشده باشد، SQL Server به صورت پیش فرض مقدار Mesaure پیش فرض را بدون اعمال هیچ بعدی نمایش می‌دهد.

خروجی دستور بالا مشابه تصویر زیر می‌باشد. 

حال دستور زیر را اجرا می‌کنیم :

Select
From [Adventure Works]
Where [Measures].[Reseller Sales Amount]

تصویر خروجی به صورت زیر می‌باشد : 

شاید باز هم تعجب کنید. نوشتن نام یک شاخص به جای عبارت شرط؟! آیا خروجی عبارات شرطی نباید Boolean باشند؟

خیر. اگر چنین پرسش هایی در ذهن شما ایجاد شده باشد، به دلیل مقایسه‌ی MDX با T/SQL می‌باشد. در اینجا شرط Where بر روی ردیف‌های جدول مدل رابطه ای اعمال نمی‌شود و عملا بیانگر واکشی اطلاعات از مدل چند بعدی می‌باشد. با اعمال شرط فوق به SSAS اعلام کرده ایم که خروجی بر اساس شاخص [Measures].[Reseller Sales Amount] باشد. با توجه به این که شاخص انتخاب شده با شاخص پیش فرض یکی می‌باشد خروجی با حالت قبل تفاوتی نخواهد کرد.

برای درک بهتر، کوئری زیر را اجرا کنید :

Select
From [Adventure Works]
where [Measures].[Internet Sales Amount]

استفاده از این شرط سبب استفاده نشدن از شاخص پیش فرض می شود . به عبارت دیگر این کوئری دارای سرجمع مبلغ فروش اینترنتی می باشد.

دستور زیر را اجرا کنید :

Select
[Measures].[Reseller Sales Amount] on columns
From [Adventure Works]

با اعمال یک شاخص خاص در ستون ، عملا فیلترینگ انجام می شود 

استفاده از یک دایمنشن در ستون :

دستور زیر را اجرا کنید

Select
[Date].[Calendar].[Calendar Year] on columns
From [Adventure Works]

خروجی به شکل زیر خواهد بود 

همان طور که مشاهده می‌کنید خروجی دارای چندین ستون می‌باشد و دارای مقادیری در هر ستون. اما این مقادیر از کجا آمده اند؟

همواره این نکته را به خاطر بسپارید که در صورت عدم ذکر نام یک Measure در کوئری ، SSAS از Measure پیش فرض استفاده می‌کند. حال کوئری فوق میزان فروش نمایندگان ( Reseller Sales Amount ) را در هر سال نمایش می‌دهد.

سوال بعدی این می‌باشد که این سال‌ها از کجا آمده اند؟ خوب برای درک بهتر این مورد می‌توانیم مانند تصویر زیر به دایمنشن Date رفته و در ساختار سلسله مراتبی ، اعضای سطح [Date].[Calendar].[Calendar Year] را مشاهده کنیم. 

ایجاد سرجمع ستون‌ها :

کوئری زیر را اجرا نمایید

Select

{[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns

From [Adventure Works]

بعد از اجرا تصویر زیر را خواهید دید : 

سوال اول این می‌باشد که کاربرد {} در انتخاب دایمنشن‌ها چیست؟ در پاسخ می‌توان گفت که اگر شاخص ها یا بعد ها ، مرتبط به یک سلسله مراتب باشند آنها را در یک {} قرار می دهیم ولی اگر سلسله مراتب متفاوت باشد، یا بعد و شاخص باشند باید در () قرار بگیرند .

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

استفاده از دایمنشن و Measure در سطر و ستون مجرا :

کوئری زیر را اجرا نمایید

Select
{[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns,
[Product].[Product Categories].[Category] on rows
From [Adventure Works]

خروجی مشابه شکل زیر می‌باشد 

در مثال فوق از بعد‌ها در ستون و همزمان، نمایش نوع دسته بندی محصولات در ردیف‌ها استفاده شده است. به عبارت دیگر نتیجه عبارت است از فروش نماینگان فروش ( Reseller Sales Amount ) براساس هر سال به تفکیک نوع دسته بندی محصول فروخته شده.

(کسانی که چنین گزارشی را با استفاده از T/SQL نوشته اند، احتمالا از آسانی نوشتن این گزارش توسط MDX ‌ها شگفت زده شده اند.)

قراردادن فیلد سرجمع در ردیف :

برای این منظور کوئری زیر را اجرا نمایید

Select
{[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns,
{[Product].[Product Categories].[Category],[Product].[Product Categories]}on rows
From [Adventure Works]

خروجی به صورت زیر می‌باشد 

نحوه‌ی نمایش سرجمع در ردیف، مشابه نمایش سرجمع در ستون می‌باشد.

استفاده از تابع non empty  :

برای حذف ستون هایی که کاملا دارای مقدار null می‌باشند به صورت زیر عمل می‌کنیم :

Select
non empty {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns ,
{[Product].[Product Categories].[Category],[Product].[Product Categories]} on rows
From [Adventure Works]

خروجی به صورت زیر می‌باشد:

انتخاب دو دایمنشن در سطر و ستون و مشخص نمودن یک Measure خاص برای کوئری :

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

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

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

در بالا میزان فروش اینترنتی برای دسته بندی محصولات و در سال‌های مختلف ارائه و همچنین سرجمع ستون و سطر نیز نمایش داده شده است. 

در صورتیکه بخواهیم ستون و سطرهایی را که دارای مقدار null در تمامی آن سطر یا ستون می‌باشند، حذف کنیم به صورت زیر عمل می‌کنیم:

Select
non empty {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns,
non empty {[Product].[Product Categories].[Category],[Product].[Product Categories]} on rows
From [Adventure Works]
Where [Measures].[Internet Sales Amount]

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

Select
[Sales Channel] on columns
From [Adventure Works]

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

Select
[Product] on columns
From [Adventure Works]

استفاده از فیلدهای یک دایمنشن که دارای سلسه مراتب می باشد نیز جایز می باشد

Select
[Product].[Category] on columns
From [Adventure Works]

Select
[Product].[Category].[all]   on columns
From [Adventure Works]
--
Select
[Product].[Category].[All] on columns
From [Adventure Works]
--
Select
[Product].[Category].[(all)] on columns
From [Adventure Works]
--
Select
[Product].[Category].[all products] on columns
From [Adventure Works]

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

در صورتی که بخواهیم از یک دایمنشن تمامی Member ‌های آن را واکشی کنیم به صورت زیر عمل خواهیم کرد

Select
{[Product].[Category].members} on columns
From [Adventure Works]

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

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

Select
[Product].[Category].[Category].members
-- dimension.hierarchy.level.members
on columns
From [Adventure Works]

اگر بخواهیم دو ستون را داشته باشیم که هر دو برای یک دایمنشن می‌باشند باید از {} استفاده کرد . دستور اول خطا خواهد داشت.

Select
[Product].[Category].[Category].members,[Product].[Category].[All Products] on columns
From [Adventure Works]

در دستور دوم با استفاده از {} خروجی نمایش داده می‌شود که عبارت است از تمامی اعضای سطح [Product].[Category].[Category]. به همراه سرجمع تمامی محصولات.

Select
{[Product].[Category].[Category].members,[Product].[Category].[All Products]} on columns
From [Adventure Works]

یک راه کوتاه‌تر برای انتخاب تمامی اعضا و سرجمع آنها

Select
{[Product].[Category].[Category],[Product].[Category]}
on columns
From [Adventure Works]

می توان از کلمات Members, All X استفاده نکرد.

انتخاب اولین دسته بندی محصول البته این ترتیب بر اساس Key Columns  در   SSAS می باشد .

Select
[Product].[Category].&[1]
on columns
From [Adventure Works]

انتخاب دقیق یک عضو در خروجی

Select
[Product].[Category].[Bikes]
on columns
From [Adventure Works]

انتخاب دو عضو از یک دایمنشن

Select
{[Product].[Category].[Bikes],[Product].[Category].[Clothing]}
on columns
From [Adventure Works]

واکشی تمامی دسته بندی محصولات بر اساس Measure پیش فرض :

Select
[Product].[Product Categories].members
on columns
From [Adventure Works]

در صورتیکه بخواهیم دو Dimension مختلف را در یک ستون یا سطر بیاوریم باید از Join استفاده کنیم. بنابر این دو دستور زیر با خطا روبرو می‌شوند

Select
[Product].[Product Categories],[Product].[Category]
on columns
From [Adventure Works]

Go

Select
{[Product].[Product Categories],[Product].[Category]}
on columns
From [Adventure Works]

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

با بررسی مثال فوق به نتایج زیر خواهیم رسید.

1. امکان استفاده از دو سلسله مراتب مختلف از یک دایمنشن در یک  Axis وجود ندارد . مگر اینکه آنها را باهمدیگر  CrossJoin کنیم .

2. امکان استفاده از دو سلسله مراتب مختلف از یک دایمنشن در دو Axis مختلف وجود دارد .

ترتیب انتخاب Axis ‌ها به صورت زیر می‌باشد:

1. Columns

2. Rows

 برای مشخص شدن موضوع کوئری زیر را اجرا کنید

Select
[Product].[Product Categories].members
on rows
From [Adventure Works]

نمی‌توانیم ردیفی را واکشی کنیم بدون اینکه ستونی برای کوئری مشخص کرده باشیم.

البته می‌توان ستون خالی ایجاد نماییم مانند مثال زیر :

Select
{} on columns,
[Product].[Product Categories].members
on rows
From [Adventure Works]

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

در مقالات بعدی به ادامه‌ی مطالب MDX Query خواهیم پرداخت.

نظرات مطالب
پیاده سازی JSON Web Token با ASP.NET Web API 2.x
- اگر فکر می‌کنید که 2 بار چک کردن به ازای هر درخواست زیاد هست، احتمالا با ASP.NET Identity کار نکردید! در ASP.NET Identity اگر بررسی اعتبار کاربر را به ازای هر درخواست رسیده فعال کنید (بجای مقدار پیش‌فرض چند دقیقه‌ای آن، این مقدار را صفر کنید تا به ازای هر درخواست انجام شود)، همین یک مورد 5 کوئری را شامل می‌شود. برای نمونه در ASP.NET Core 2.X این بررسی‌ها شامل 5 کوئری به جداول AspNetUser, AspNetUserClaims, AspNetUserRoles, AspNetRoles, AspNetRoleClaims هستند.
- 2 بار بررسی بانک اطلاعاتی برای بانک‌های اطلاعاتی امروزی هیچ سرباری ندارد و ضمن اینکه خودشان مباحث کش کردن اطلاعات ویژه‌ای را هم برای کوئری‌های پر استفاده دارند؛ مانند buffer cache در SQL Server که تا حد مصرف حافظه‌ی کل سرور هم می‌تواند پیش رود.
- استفاده از متغیرهای استاتیک و حافظه‌ی سرور برای کش کردن، مقیاس پذیر نیست. در این موارد روش توصیه شده، استفاده از بانک اطلاعاتی Key/Value فوق سریع Redis هست. فقط مشکل تمام کش‌ها، هماهنگ سازی اطلاعات آن‌ها با بانک اطلاعاتی اصلی است که باید مدنظر باشند.
مطالب
استفاده از دیتابیس Sqlite در الکترون (قسمت اول)
یکی از مهمترین بخش‌های هر برنامه، بخش ذخیره و بازیابی دیتا است. برای ذخیره سازی از طریق وب و مرورگر، راه‌های مختلف زیادی چون webStorage ,Indexed DB ,Sqlite ,NeDB, و ... وجود دارند.

Sqlite دیتابیس مناسبی برای برنامه‌های چندسکویی است و عموما به عنوان اولین گزینه استفاده می‌شود. برای کار با این دیتابیس، ما از ماژول sql.js که یکی از ماژول‌های معروف در جاوااسکریپت است، استفاده می‌کنیم. برای نصب آن از طریق npm، به شکل زیر اقدام می‌کنیم:
npm install sql.js --save
سپس کد همیشگی زیر را برای آغاز، در فایل index.js وارد می‌کنیم:
const{app,BrowserWindow}=require("electron");

let win;
function onLoad()
{
  win=new BrowserWindow({
    width:800,
    height:600
  });
  win.loadURL(`file://${__dirname}/index.html`);
}

app.on("ready",onLoad());
  ابتدا باید بررسی کنیم که آیا دیتابیس از قبل موجود است یا خیر و اگر موجود نبود، برای اولین بار آن را بسازیم. برای بررسی وجود یک فایل نیز می‌توانیم از چند دستور مختلف استفاده کنیم. path.exists و fs.exists، دو عدد از آن‌ها می‌باشند که هر دوی آنان به صورت غیرهمزمان هستند و پارمتر اولشان نام فایل، به همراه مسیر (یا تنها مسیر) است و پارامتر دوم هم یک تابع callback است که به عنوان پارامتر، جواب را بر می‌گرداند. برای استفاده از حالت همزمان، عبارت Sync را به انتهای نام متدها اضافه کنید. نحوه استفاده از آن به شکل زیر است:
var path=require("path");
path.exists('filepath",(status)=>
{
....
});
var status=path.existsSync("file");
//===============================
var fs=require("fs");
fs.exists('filepath",(status)=>
{
....
});
var status=path.existsSync("file");
ولی بهتر است بدانید که تاریخ انقضای دو دستور بالا سر آمده است و الان از دستور fs.stat استفاده می‌شود. این متد هم به دو شکل همزمان و غیرهمزمان وجود دارد:
fs.stat('foo.txt', function(err, stat) {
    if(err == null) {
        console.log('فایل موجوده');
    } else if(err.code == 'ENOENT') {
        // فایل وجود نداره
        fs.writeFile('log.txt', 'Some log\n');
    } else {
        //خطای دیگری رخ داده است
    }
});
برای استفاده از حالت همزمان هم کد را به شکل زیر بنویسید:
try {
  stats = fs.statSync(path);
  console.log("File exists.");
}
catch (e) {
  console.log("File does not exist.");
}
سپس کد زیر را در فرآیند اصلی وارد می‌کنیم:
const fs = require('fs');
const sql = require('sql.js');

  dbPath = './mydb.sqlite';
  dbExists=false;

try {
  dbExists = fs.statSync(dbPath);
}
catch (e) {
}

if(!dbExists)
{
  //create Database
var sqlStr=fs.readFileSync("./sql.txt");
var db = new sql.Database();
db.run(String(sqlStr));

//write to disk
var data=db.export();
var buffer=new Buffer(data);
fs.writeFileSync(dbPath,buffer);
}
else{
  var buffer = fs.readFileSync(dbPath);
  var db = new sql.Database(buffer);
}
ابتدا بررسی می‌کنیم که آیا فایلی با نام test.sqlite در مسیر جاری است یا خیر. در صورتی که نباشد، کد داخل شرط اجرا می‌شود. در اولین خط شرط، فایلی را با نام sql.txt که شامل محتوای زیر است، می‌خوانیم:
CREATE TABLE numbers (
    id     INT          PRIMARY KEY
                        UNIQUE
                        NOT NULL,
    fname  VARCHAR (20) NOT NULL,
    lname  VARCHAR (30) NOT NULL,
    number VARCHAR (15) NOT NULL
);
insert into numbers values(1,'ali','yeganeh','03111223344');
insert into numbers values(2,'xxx','yyy','45454555');
سپس در خطوط بعدی دیتابیس جدیدی را ایجاد می‌کنیم و دستور sql را با متد run اجرا می‌کنیم. دستوراتی که متد run اجرا می‌کند، شامل خروجی نیستند. پس دستوراتی را که نیاز به خروجی ندارند، به این متد بسپارید. سپس از این دیتابیس، یک شیء خروجی را دریافت میکنم و با بافر کردن، آن را در یک فایل ذخیره می‌کنیم. در صورتی هم که از قبل دیتابیس وجود داشته باشد، بافر خوانده شده را مستقیما به سازنده Database می‌دهیم.
بعد از آن نیاز است تا دیتابیس در دسترس Render Process‌ها قرار بگیرد که در مقاله "شیوه کدنویسی در الکترون " در مورد global صحبت کرده‌ایم و نحوه استفاده از آن را فرا گرفتیم:
global.db=db;

در پایان اجرای برنامه لازم است که دیتابیس توسط دستور close بسته شود. سپس کد زیر را در رویداد windows-all-closed می‌نویسیم:
app.on('window-all-closed', () => {
  db.close();
  if (process.platform !== 'darwin') {
    app.quit();
  }
});
در این کد گفته‌ایم که موقعی که تمام پنجره‌های برنامه بسته شدند، دیتابیس را نیز ببند.
(چند مورد خارج از بحث): کد بعدی که مورد استفاده قرار گرفته است و در مقالات قبلی در مورد آن صحبت نکرده‌ایم این است که در سیستم‌های مک، وضعیت به این قرار است که اگر شما برنامه را ببیندید، آن برنامه بسته نشده و در پس زمینه فعال است و می‌توانید آن را از طریق dock اطراف صفحه، مجددا فعال کنید. ولی با نوشتن کد بالا، ما این وضعیت را اعلام کرده‌ایم که اگر تمامی پنجره‌ها بسته شدند، کل برنامه را ببند.

همچنین بسیار خوب است که کد زیر را هم همیشه اضافه کنید:
win.on('closed', () => {
    win = null;
  });
موقعی که پنجره مربوطه بسته شود، متغیری که به پنجره اشاره می‌کند، در حافظه می‌ماند. پس بهتر است که این مقدار حافظه را رها کنید تا Garbage Collector اقدام به حذف آن در حافظه کند.
پس اگر این کد را نوشتید، وضعیت سیستم عامل مک را به خاطر داشته باشید و مجبور هستید کد زیر را نیز اضافه کنید:
app.on('activate', () => {
  if (win === null) {
    createWindow();
  }
});
در این صورت اگر کاربر پنجره را از طریق Dock مجددا فعال کرد، پنجره برنامه شما نیز مجددا نمایش داده خواهدشد.

بعد از اینکه دیتابیس را به شیء global دادیم، در صفحه html کد زیر را وارد می‌کنیم:
<html>
  <head>
    <script src="./jquery.min.js"></script>
    <link href="./bootstrap-3.3.6-dist/css/bootstrap.min.css" rel="stylesheet"></link>
    <meta charset="utf-8">
    <title></title>
    <script>
    const {remote}=require("electron");
    let db=remote.getGlobal("db");
    </script>
  </head>
  <body>

<table id="people" class="table table-hover table-striped">
<th>
  <tr>
    <td>First Name</td>
    <td>last Name</td>
    <td>Phone Number</td>
  </tr>
</th>
<tbody>

</tbody>
</table>
  </body>
</html>
در این کد یک جدول داریم و قصد ما این است که آن دو سطر را که در ابتدا اضافه کردیم، در آن نمایش دهیم. چیزی که در این کدها قابل ملاحظه است، این است که ما از بسته‌های بوت استرپ و جی کوئری استفاده می‌کنیم. در ادامه کدهای زیر را در تگ اسکریپت  وارد می‌کنیم:
$(document).ready(()=>
{
  //show data
var tableBody=$("#people");

db.each("select * from numbers",(row)=>{
  let rowTemplate=`<tr><td>${row.fname}</td><td>${row.lname}</td><td>${row.number}</td></tr>`;
  tableBody.append(rowTemplate);
});
متد each برای مواقعی مناسب است که شما تعدادی سطر را برای بازگشت دارید و callback آن، به ازای هر سطر اجرا می‌شود و با استفاده از یک template string سطر سازی را انجام داده و آن را با استفاده از توابع جی کوئری به انتهای جدول اضافه می‌کنیم.
حال وقت آن رسیده است که خروجی کار را ببینیم. پس کد npm start را اجرا می‌کنیم. همانطور که می‌بینید خروجی به راحتی نمایش داده می‌شود. در مقاله بعدی بیشتر در این مورد صحبت می‌کنیم.
نظرات مطالب
استفاده‌ی گسترده از DateTimeOffset در NET Core.
یک نکته‌ی تکمیلی: بانک اطلاعاتی SQLite از نوع داده‌ی DateTimeOffset پشتیبانی نمی‌کند

SQLite به صورت توکار از هیچ نوع داده‌ای خاصی برای کار با زمان یا تاریخ پشتیبانی نمی‌کند؛ اما متدهایی را برای کار با آن‌ها به همراه دارد و در این بین، EF Core فقط نوع داده‌ای DateTime را برای آن به خوبی پشتیبانی می‌کند. در سایر حالات استفاده‌ی از DateTimeOffset، پیام عدم امکان ترجمه‌ی این کوئری LINQ را به SQL، مشاهده خواهید کرد. به همین جهت برای کار بدون دردسر با زمان در SQLite و EF Core، بهتر است از همان DateTime استفاده کرد.
این روش‌ها را نیز مدنظر داشته باشید:
- در این بانک اطلاعاتی برای مثال می‌توان تاریخ را به صورت زیر ذخیره و بازیابی کرد:
((DateTimeOffset)value).Ticks.ToString()

- و یا می‌توان برای آن تبدیلگر نوشت:
namespace MySQLite
{
    public class SQLiteDbContext : DbContext
    {
        public SQLiteDbContext(DbContextOptions options) : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            addDateTimeOffsetConverter(builder);
        }

        private static void addDateTimeOffsetConverter(ModelBuilder builder)
        {
            // SQLite does not support DateTimeOffset
            foreach (var property in builder.Model.GetEntityTypes()
                                                  .SelectMany(t => t.GetProperties())
                                                  .Where(p => p.ClrType == typeof(DateTimeOffset)))
            {
                property.SetValueConverter(
                     new ValueConverter<DateTimeOffset, DateTime>(
                          convertToProviderExpression: dateTimeOffset => dateTimeOffset.UtcDateTime,
                          convertFromProviderExpression: dateTime => new DateTimeOffset(dateTime)
                    ));
            }

            foreach (var property in builder.Model.GetEntityTypes()
                                                  .SelectMany(t => t.GetProperties())
                                                  .Where(p => p.ClrType == typeof(DateTimeOffset?)))
            {
                property.SetValueConverter(
                     new ValueConverter<DateTimeOffset?, DateTime>(
                          convertToProviderExpression: dateTimeOffset => dateTimeOffset.Value.UtcDateTime,
                          convertFromProviderExpression: dateTime => new DateTimeOffset(dateTime)
                    ));
            }
        }
    }
}

// یک نمونه‌ی دیگر
private static readonly ValueConverter<object, string> DateTimeOffsetToStringConverter =
    new ValueConverter<object, string>(
              v => ((DateTimeOffset)v).ToString(@"yyyy\-MM\-dd HH\:mm\:ss.FFFFFFFzzz", CultureInfo.InvariantCulture),
              v => DateTimeOffset.Parse(v, CultureInfo.InvariantCulture));
مطالب
breeze js به همراه ایجاد سایت آگهی قسمت اول
با قدرت گرفتن جاوا اسکریپت، نیازهایی مانند کوئری گرفتن در سمت کلاینت، کش کردن داده‌ها در سمت کلاینت، ردیابی تغییرات، اعتبارسنجی مدلها، ذخیره کردن گروهی از عملیات‌ها (Save Batch)، تعامل با Web Api .Net یا Node Js، قابلیت کار کردن با No Sql و... افزایش یافته است و تمام این کارها توسط breeze  امکان پذیر میباشد. breeze  با هر سرویس دهنده‌ای که بتواند از  طریق http و با فرمت json عملیات خود را انجام دهد، میتواند ارتباط برقرار کند.
breeze مطابق با استاندارد ECMAScript 5 نوشته شده‌است؛ از این‌رو بر روی اکثر مرورگرهای جدید به خوبی اجرا میشود.

  برای مرورگرهای قدیمی می‌توان کتابخانه es5-shim.js and es5-sham.js را قبل از تگ اسکریپت breeze js قرار داد.
در این قسمت به برخی از  توانایی های  breeze  اشاره کوتاهی میکنیم و در مقالات بعدی پیاده سازی آن‌را در قالب angular js /Web Api .net خواهیم داشت:
1- کوئری‌های breeze اکثر امکانات  linq را دارا می‌باشد؛ مانند شرطهای ساده، شرط‌های پیچیده، Sorting ،Paging،Projections  و Filter کردن برخی از پراپرتی‌ها که بسیار مشابه  linq میباشد:
var query = breeze.EntityQuery
           .from("Customers")
           .where("CompanyName", "startsWith", "A")
           .orderBy("CompanyName");
2- کوئری‌های breeze با promises عملیات خود را انجام میدهند:
var promise = manager.executeQuery(query)
              .then(querySucceeded)
              .fail(queryFailed);
* breeze مانند  entity framework  قابلیت ردیابی تغییرات را دارا میباشد:
if (manager.hasChanges()) {
    manager.saveChanges().then(saveSucceeded).fail(saveFailed);
}
// listen for any change to a customer 
customer.entityAspect.propertyChanged.subscribe(somethingHappened);
3- انقیاد داده‌ها با Angular / Knockout  / Backbone را دارا میباشد:
<!-- Angular template -->
<li data-ng-repeat="emp in employees">
    <label>{{emp.FirstName}}</label>
    <label>{{emp.LastName}}</label>
</li>
// bound to employees from query
manager.executeQuery(breeze.EntityQuery.from("Employees"))
       .then(function(data) { $scope.employees = data.results; });
4- درbreeze امکان  کوئری گرفتن به همراه entity‌های مرتبط وجود دارد:
/* Query with related entities using expand */
// query for orders of customers whose name begins "Alfreds"
// include their customers & child details & their detail products
breeze.EntityQuery.from("Orders")
   .where("Customer.CompanyName", "startsWith", "Alfreds")
   .expand("Customer, OrderDetails.Product")
   .using(manager)
   .execute().then(querySucceeded).fail(queryFailed);
5- در breeze  کوئری‌ها میتوانند از سرور و یا کش درخواست شوند:
// execute query asynchronously on the server
manager.executeQuery(query).then(querySuccess).fail(queryFail);
 
// execute query synchronously on local cache
var customers = manager.executeQueryLocally(query)
مطالب
آشنایی با قابلیت FileStream اس کیوال سرور 2008 - قسمت سوم

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

alter table tbl_files set(filestream_on ='default')

go
alter table tbl_files
add

[systemfile] varbinary(max) filestream null ,
FileId uniqueidentifier not null rowguidcol unique default (newid())
go

در ادامه جدول tblFiles قسمت قبل را در نظر بگیرید:

CREATE TABLE [tblFiles](
[FileId] [uniqueidentifier] ROWGUIDCOL NOT NULL,
[Title] [nvarchar](255) NOT NULL,
[SystemFile] [varbinary](max) FILESTREAM NULL,
UNIQUE NONCLUSTERED
(
[FileId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] FILESTREAM_ON [fsg1]

ALTER TABLE [dbo].[tblFiles] ADD DEFAULT (newid()) FOR [FileId]
GO

نحوه‌ی افزودن رکوردی جدید به جدول tblFiles :

INSERT INTO [tblFiles]
(
[Title],
[SystemFile]
)
VALUES
(
'file-1',
CAST('data data data' AS VARBINARY(MAX))
)
در اینجا سعی کرده‌ایم یک رشته ساده را در فیلدی از نوع فایل استریم ذخیره کنیم که روش کار به صورت فوق است. از آنجائیکه مقدار پیش فرض FileId را هنگام تعریف جدول به NEWID تنظیم کرده‌ایم، نیازی به ذکر آن نیست و به صورت خودکار محاسبه و ذخیره خواهد شد.
اگر کنجکاو باشید که این فایل اکنون کجا ذخیره شده و نحوه‌ی مدیریت آن توسط اس کیوال سرور به چه صورتی است، فقط کافی است به مسیری که هنگام افزودن گروه فایل‌ها و فایل مربوطه در تنظیمات خواص دیتابیس در قسمت قبل مشخص کردیم، مراجعه کرد (شکل زیر).



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

using System;
using System.IO;
using System.Data.SqlClient;
using System.Data;

namespace FileStreamTest
{
class CFS
{
/// <summary>
/// افزودن رکورد به جدول حاوی ستونی از نوع فایل استریم
/// </summary>
/// <param name="filePath">مسیر فایل</param>
/// <param name="title">عنوانی دلخواه</param>
public static void AddNewRecord(string filePath, string title)
{
//آیا فایل وجود دارد؟
if (!File.Exists(filePath))
throw new FileNotFoundException(
"لطفا مسیر فایل معتبری را مشخص نمائید", filePath);

//خواندن اطلاعات فایل در آرایه‌ای از بایت‌ها
byte[] buffer = File.ReadAllBytes(filePath);

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//ساخت عبارت افزودن پارامتری
using (SqlCommand objSqlCmd = new SqlCommand(
"INSERT INTO [tblFiles]([Title],[SystemFile]) VALUES(@title , @file)",
objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.Text;

//تعریف وضعیت پارامترها و مقدار دهی آن‌ها
objSqlCmd.Parameters.AddWithValue("@title", title);
objSqlCmd.Parameters.AddWithValue("@file", buffer);

//اجرای فرامین
objSqlCmd.ExecuteNonQuery();
}

//پایان تراکنش
objSqlTran.Commit();
}
}
}

/// <summary>
/// دریافت اطلاعات فایل ذخیره شده به صورت آرایه‌ای از بایت‌ها
/// </summary>
/// <param name="fileId">کلید مورد استفاده</param>
/// <returns></returns>
public static byte[] GetDataFromDb(string fileId)
{
byte[] data = null;

using (SqlConnection objConn = new SqlConnection())
{
//کوئری اس کیوال پارامتری جهت دریافت محتویات فایل
string cmdText = "SELECT SystemFile FROM tblFiles WHERE FileId=@id";
using (SqlCommand objCmd = new SqlCommand(cmdText, objConn))
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objConn.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objConn.Open();

//تنظیم کردن وضعیت و مقدار پارامتر تعریف شده در کوئری
objCmd.Parameters.AddWithValue("@id", fileId);

//اجرای فرامین و دریافت فایل
using (SqlDataReader objread = objCmd.ExecuteReader())
{
if (objread != null)
if (objread.Read())
{
if (objread["SystemFile"] != DBNull.Value)
data = (byte[])objread["SystemFile"];
}
}
}
}

return data;
}
}
}

مثالی در مورد روش استفاده از کلاس فوق :

using System.IO;

namespace FileStreamTest
{
class Program
{
static void Main(string[] args)
{
CFS.AddNewRecord(@"C:\filest05.PNG", "test1");

//آی دی رکورد ذخیره شده در دیتابیس برای مثال
byte[] data = CFS.GetDataFromDb("BB848D45-382C-4D95-BF4E-52C3509407D4");
if (data != null)
{
File.WriteAllBytes(@"C:\tst.PNG", data);
}
}
}
}
روش فوق با روش متداول افزودن یک فایل به دیتابیس اس کیوال سرور هیچ تفاوتی ندارد و این‌جا هم بدون مشکل کار می‌کند. اطلاعات نهایی به صورت فایل‌هایی بر روی سیستم که توسط اس کیوال سرور مدیریت خواهند شد و با جدول شما یکپارچه‌اند، ذخیره می‌شوند.

در روش دیگری که در اکثر مقالات مرتبط مورد استفاده است، از شیء SqlFileStream کمک گرفته شده و نحوه‌ی انجام آن نیز به صورت زیر می‌باشد.
در ابتدا دو رویه ذخیره شده زیر را ایجاد می‌کنیم:

CREATE PROCEDURE [AddFile](@Title NVARCHAR(255), @filepath VARCHAR(MAX) OUTPUT)
AS
BEGIN
SET NOCOUNT ON;

DECLARE @ID UNIQUEIDENTIFIER
SET @ID = NEWID()

INSERT INTO [tblFiles]
(
[FileId],
[title],
[SystemFile]
)
VALUES
(
@ID,
@Title,
CAST('' AS VARBINARY(MAX))
)

SELECT @filepath = SystemFile.PathName()
FROM tblFiles
WHERE FileId = @ID
END
GO

CREATE PROCEDURE [GetFilePath](@Id VARCHAR(50))
AS
BEGIN
SET NOCOUNT ON;

SELECT SystemFile.PathName()
FROM tblFiles
WHERE FileId = @ID
END
در رویه ذخیره شده AddFile ، ابتدا رکوردی بر اساس عنوان دلخواه ورودی با یک فایل خالی ایجاد می‌شود. سپس مسیر سیستمی این فایل را در آرگومان خروجی filepath قرار می‌دهیم. SystemFile.PathName از اس کیوال سرور 2008 جهت فیلدهای فایل استریم به اس کیوال سرور اضافه شده است. از این مسیر در برنامه خود جهت نوشتن بایت‌های فایل مورد نظر در آن توسط شیء SqlFileStream استفاده خواهیم کرد.
رویه ذخیره شده GetFilePath نیز تنها مسیر سیستمی فایل استریم ذخیره شده را بر می‌گرداند.
به این ترتیب کدهای برنامه به صورت زیر تغییر خواهند کرد:

using System.Data.SqlClient;
using System.Data;
using System.Data.SqlTypes;
using System.IO;

namespace FileStreamTest
{
class CFSqlFileStream
{
/// <summary>
/// افزودن رکورد به جدول حاوی ستونی از نوع فایل استریم
/// </summary>
/// <param name="filePath">مسیر فایل</param>
/// <param name="title">عنوانی دلخواه</param>
public static void AddNewRecord(string filePath, string title)
{
//آیا فایل وجود دارد؟
if (!File.Exists(filePath))
throw new FileNotFoundException(
"لطفا مسیر فایل معتبری را مشخص نمائید", filePath);

//خواندن اطلاعات فایل در آرایه‌ای از بایت‌ها
byte[] buffer = File.ReadAllBytes(filePath);

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//استفاده از رویه ذخیره شده افزودن فایل
using (SqlCommand objSqlCmd = new SqlCommand(
"AddFile", objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.StoredProcedure;

//مشخص ساختن وضعیت و مقدار پارامتر عنوان
SqlParameter objSqlParam1 = new SqlParameter("@Title", SqlDbType.NVarChar, 255);
objSqlParam1.Value = title;

//مشخص ساختن پارامتر خروجی رویه ذخیره شده
SqlParameter objSqlParamOutput = new SqlParameter("@filepath", SqlDbType.VarChar, -1);
objSqlParamOutput.Direction = ParameterDirection.Output;

//افزودن پارامترها به شیء کامند
objSqlCmd.Parameters.Add(objSqlParam1);
objSqlCmd.Parameters.Add(objSqlParamOutput);

//اجرای رویه ذخیره شده
objSqlCmd.ExecuteNonQuery();

//و سپس دریافت خروجی آن
string Path = objSqlCmd.Parameters["@filepath"].Value.ToString();

//زمینه تراکنش فایل استریم موجود را دریافت کرده و از آن برای نوشتن محتویات فایل استفاده خواهیم کرد
//این مورد نیز یکی از تازه‌های اس کیوال سرور 2008 است
using (SqlCommand objCmd = new SqlCommand(
"SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()", objSqlCon, objSqlTran))
{
byte[] objContext = (byte[])objCmd.ExecuteScalar();
using (SqlFileStream objSqlFileStream =
new SqlFileStream(Path, objContext, FileAccess.Write))
{
objSqlFileStream.Write(buffer, 0, buffer.Length);
}
}
}

objSqlTran.Commit();
}
}
}

/// <summary>
/// دریافت اطلاعات فایل ذخیره شده به صورت آرایه‌ای از بایت‌ها
/// </summary>
/// <param name="fileId">کلید مورد استفاده</param>
/// <returns></returns>
public static byte[] GetDataFromDb(string fileId)
{
byte[] buffer = null;

using (SqlConnection objSqlCon = new SqlConnection())
{
//todo: کانکشن استرینگ باید از یک فایل کانفیگ خوانده شود
objSqlCon.ConnectionString =
"Data Source=(local);Initial Catalog=testdb2009;Integrated Security = true";
objSqlCon.Open();

//شروع یک تراکنش
using (SqlTransaction objSqlTran = objSqlCon.BeginTransaction())
{
//استفاده از رویه ذخیره شده دریافت مسیر فایل
using (SqlCommand objSqlCmd =
new SqlCommand("GetFilePath", objSqlCon, objSqlTran))
{
objSqlCmd.CommandType = CommandType.StoredProcedure;

//مشخص ساختن پارامتر ورودی رویه ذخیره شده و مقدار دهی آن
SqlParameter objSqlParam1 = new SqlParameter("@ID", SqlDbType.VarChar, 50);
objSqlParam1.Value = fileId;
objSqlCmd.Parameters.Add(objSqlParam1);

//اجرای رویه ذخیره شده و دریافت مسیر سیستمی فایل استریم
string path = string.Empty;
using (SqlDataReader sdr = objSqlCmd.ExecuteReader())
{
sdr.Read();
path = sdr[0].ToString();
}

//زمینه تراکنش فایل استریم موجود را دریافت کرده و از آن برای خواندن محتویات فایل استفاده خواهیم کرد
//این مورد نیز یکی از تازه‌های اس کیوال سرور 2008 است
using (SqlCommand objCmd = new SqlCommand(
"SELECT GET_FILESTREAM_TRANSACTION_CONTEXT()", objSqlCon, objSqlTran))
{
byte[] objContext = (byte[])objCmd.ExecuteScalar();

using (SqlFileStream objSqlFileStream =
new SqlFileStream(path, objContext, FileAccess.Read))
{
buffer = new byte[(int)objSqlFileStream.Length];
objSqlFileStream.Read(buffer, 0, buffer.Length);
}
}
}

objSqlTran.Commit();
}
}

return buffer;
}
}
}
در پایان برای تکمیل بحث می‌توان به مقاله‌ی مرجع زیر مراجعه کرد:
FILESTREAM Storage in SQL Server 2008

مطالب
چگونه یک عبارت sql را فرمت شده نمایش دهیم؟
در مورد کتابخانه‌ی SQLDom مطالبی را پیشتر در این سایت مطالعه کرده‌اید (^ و ^). یکی دیگر از کاربردهای آن، فرمت عبارات SQL است. برای مثال تبدیل عبارتی مانند
SELECT * FROM tb1 WHERE x1 = '12';
به نمونه‌ی فرمت شده‌ی آن:
SELECT *
FROM tb1
WHERE x1 = '12';
برای اینکار می‌توان از کلاس ذیل کمک گرفت:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.TransactSql.ScriptDom;
 
namespace SqlDomAnalyzer.Core
{
    public static class PrettyPrintTSql
    {
        public static string FormatTSql(string tSql)
        {
            IList<ParseError> errors;
            TSqlScript sqlFragment;
            using (var reader = new StringReader(tSql))
            {
                var parser = new TSql120Parser(initialQuotedIdentifiers: true);
                sqlFragment = (TSqlScript)parser.Parse(reader, out errors);
            }
 
            if (errors != null && errors.Any())
            {
                var sb = new StringBuilder();
                foreach (var error in errors)
                    sb.AppendLine(error.Message);
 
                throw new InvalidOperationException(sb.ToString());
            }
 
            var sql110ScriptGenerator = new Sql120ScriptGenerator(new SqlScriptGeneratorOptions
            {
                SqlVersion = SqlVersion.Sql120
            });
            string finalScript;
            sql110ScriptGenerator.GenerateScript(sqlFragment, out  finalScript);
            return finalScript;
        }
    }
}
در اینجا ابتدا عبارت SQL ورودی Parse شده و سپس به کتابخانه‌ی تولید اسکریپت ScriptDom ارسال می‌شود. خروجی آن، یک خروجی فرمت شده‌است.

نکته‌ی جالب دیگری که در اینجا وجود دارد، تهیه‌ی یک خروجی همواره یک شکل است. برای نمونه سه عبارت SQL زیر را در نظر بگیرید:
SELECT * from tb1 WHERE x1 = '12';
SELECT * from tb1 where x1 = '12';
select * from tb1 WHERE x1 = '12';
در اینجا در عبارت اول، from با حروف کوچک نمایش داده شده‌است. در عبارت دوم، where نیز با حروف کوچک نمایش داده شده‌است و در عبارت سوم اینکار در مورد select نیز تکرار شده‌است.
در هر سه حالت یا هر حالت قابل تصور دیگری، خروجی SQL فرمت شده‌ی حاصل یک چنین شکلی را دارد:
SELECT *
FROM tb1
WHERE x1 = '12';

موارد کاربرد آن؟
علاوه بر نمایش زیبای SQL فرمت نشده، احتمالا برنامه‌های Profiler ایی را دیده‌اید که عنوان می‌کنند قادرند عبارات SQL همانند را تشخیص دهند (جهت یافتن Lazy loading اشتباه). یک چنین خروجی یکسانی، قابلیت تهیه Hash عبارات SQL دریافتی را میسر می‌کند؛ چون دیگر اینبار مهم نیست که اجزای تشکیل دهنده‌ی یک عبارت SQL با حروف بزرگ هستند یا کوچک و فاصله‌ی بین آن‌ها چقدر است و آیا در این بین خطوط جدیدی نیز وجود دارند و امثال آن. خروجی نهایی نرمال شده‌ی توسط Sql120ScriptGenerator همواره یک شکل است. از این دو قابلیت در برنامه‌ی DNTProfiler استفاده شده‌است.
مطالب
سری بررسی SQL Smell در EF Core - استفاده از مدل Entity Attribute Value - بخش دوم
در مطلب قبلی، مدل EAV را معرفی کردیم و گفتیم که این نوع پیاده‌سازی در واقع یک SQL Smell است؛ زیرا کوئری نویسی را سخت میکند و همچنین به دلیل عدم امکان تعریف constraints، کنترلی بر روی صحت دیتاهای وارده شد نخواهیم داشت. در نهایت با برنامه‌ای روبرو خواهیم شد که درک صحیحی از ماهیت دیتا ندارد. اما اگر در شرایطی مجبور به استفاده‌ی از این مدل هستید، بهتر است از فرمت JSON برای ذخیره‌سازی دیتای داینامیک استفاده کنید. بیشتر دیتابیس‌های رابطه‌ایی به صورت native از نوع داده‌ایی JSON پشتیبانی میکنند:  
CREATE TABLE EmployeeJsonAttributes (
  Id int NOT NULL AUTO_INCREMENT,
  EmployeeId int NOT NULL,
  Attributes json DEFAULT NULL,
  PRIMARY KEY (Id),
  FOREIGN KEY (EmployeeId) REFERENCES EmployeeEav (Id) ON DELETE CASCADE
)
همانطور که مشاهده می‌کنید در اینجا تایپ ستون Attributes، به JSON تنظیم شده است. بنابراین می‌توانیم از قابلیت‌های توکار دیتابیس (MySQL در مطلب جاری) برای ذخیره و بازیابی داده‌های JSON استفاده کنیم. در ادامه دو روش ذخیره JSON  را مشاهده میکنید: 
INSERT INTO EmployeeJsonAttributes VALUES (
101, 
  '{
  "name": "Jon",
    "lastName": "Doe",
    "dateOfBirth": "1989-01-01 10:10:10+05:30",
    "skills": [ "C#", "JS" ],
    "address":  {
  "country": "UK",
      "city": "London",
      "email": "jon.doe@example.com"
    }
  }'
)

INSERT INTO efcoresample.EmployeeJsonAttributes VALUES (
101, 
  JSON_OBJECT(
"name", "Jon", 
"lastName", "Doe",
"dateOfBirth", "1989-01-01 10:10:10+05:30",
"skills", JSON_ARRAY("C#", "JS"),
    "address", JSON_OBJECT(
  "country", "UK",
      "city", "London",
  "email", "jon.doe@example.com"
    )
  )
)

به عنوان مثال در ادامه میخواهیم کشور محل تولد یک کاربر خاص را نمایش دهیم. برای اینکار می‌توانیم از JSON_EXTRACT استفاده کنیم:
SELECT JSON_EXTRACT(Attributes, '$.address.country') as Country 
FROM EmployeeJsonAttributes
WHERE EmployeeId = 101;

-- Conutry
-- "UK"

همچنین می‌توانیم از عملگر column-path نیز به جای JSON_EXTRACT استفاده کنیم:
SELECT Attributes -> '$.address.country' as Country 
FROM EmployeeJsonAttributes
WHERE EmployeeId = 101;

-- Conutry
-- "UK"

بنابراین به راحتی می‌توانیم کوئری مطلب قبل را اینگونه بازنویسی کنیم:
SELECT EmployeeId, Attributes ->> '$.DateOfBirth' AS BirthDate FROM EmployeeJsonAttributes
WHERE Attributes ->> '$.DateOfBirth' > DATE_SUB(CURRENT_DATE(), INTERVAL 25 YEAR)
همانطور که مشاهده می‌کنید در کوئری فوق یک عملگر < دیگر نیز اضافه کرده‌ایم. هدف از آن حذف “” از خروجی نهایی می‌باشد. 

استفاده از JSON در EF Core 
متاسفانه در EF Core به صورت مستقیم نمی‌توانیم از JSON درون کلاس‌های سی‌شارپ استفاده کنیم (+ )، در نتیجه در سمت کلاس‌های سی‌شارپ باید از string استفاده کنیم و به نوعی به EF Core اطلاع دهیم که تایپ ستون موردنظرمان JSON است. در نتیجه خروجی نهایی درون دیتابیس، یک فیلد با تایپ JSON خواهد بود. برای اینکار به دو شیوه می‌توانیم تایپ ستون موردنظر را تعیین کنیم: 
// Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Employee>(entity =>
    {
        entity.Property(e => e.Attributes).HasColumnType("json");
    });
}

// Data Annotations
[Column(TypeName = "json")]
public string Attributes { get; set; }

در نهایت برای تشکیل بانک اطلاعاتی، به مدلی با ساختار زیر نیاز خواهیم داشت:
public class EmployeeJsonAttribute
{
    public int Id { get; set; }
    public virtual EmployeeEav Employee { get; set; }
    public int EmployeeId { get; set; }
    [Column(TypeName = "json")]
    public string Attributes { get; set; }
}
در اینجا به جای تعریف ستون‌ها و مقادیر داینامیک‌شان از یک فیلد از نوع رشته‌ایی با نام Attributes استفاده شده است. از آنجائیکه نوع ستون در سمت دیتابیس به JSON تنظیم خواهد شد، در نتیجه هر نوع ساختار JSON معتبری را می‌توانیم درون آن ذخیره کنیم:
dbContext.EmployeeJsonAttributes.Add(new EmployeeJsonAttribute
{
    EmployeeId =  101,
    Attributes = JsonSerializer.Serialize(new
    {
        FirstName = "Sirwan",
        LastName = "Afifi",
        DateOfBirth = DateTime.Now.AddYears(-31)
    })
});

dbContext.SaveChanges();
همانطور که اشاره شده به دلیل عدم پشتیبانی از JSON در حال حاضر در EF Core امکان کوئری نویسی بر روی ستون JSON را نداریم. در همین حد که براساس فیلدهای دیگر جستجو را انجام داده و خروجی را Deserialize کنیم:
var employee = dbContext.EmployeeJsonAttributes.Find(201);
Console.WriteLine(JsonSerializer.Deserialize<Employee>(employee.Attributes).DateOfBirth);

برای نوشتن کوئری روی ستون JSON می‌توانید از Query Types  نیز استفاده کنید. 
نظرات مطالب
صفحه بندی، مرتب سازی و جستجوی پویای اطلاعات به کمک Kendo UI Grid
یک نکته‌ی تکمیلی

معادل قطعه کد ذیل که مخصوص ASP.NET MVC 5x است:
var request = JsonConvert.DeserializeObject<DataSourceRequest>(
                requestMessage.RequestUri.ParseQueryString().GetKey(0)
            );
در ASP.NET Core به صورت زیر می‌باشد (روش استخراج کل کوئری استرینگ به صورت خام و تبدیل آن به یک رشته‌ی JSON معتبر برای Deserialize شدن):
var rawQueryString = this.HttpContext.Request.QueryString.ToString();
// PM> Install-Package Microsoft.AspNetCore.WebUtilities
var rawQueryStringKeyValue = QueryHelpers.ParseQuery(rawQueryString).FirstOrDefault();
var dataString = Uri.UnescapeDataString(rawQueryStringKeyValue.Key);

var request = JsonConvert.DeserializeObject<DataSourceRequest>(dataString);