نظرات مطالب
دریافت اطلاعات از پایگاه داده بواسطه Stored Procedure در EF Core 2.0
FirstOrDefault یعنی select top 1. در SQL Server نمی‌توان یک چنین کاری را بر روی یک رویه‌ی ذخیره شده به صورت مستقیم انجام داد. یعنی نمی‌توان نوشت:
SELECT top 1 * FROM (EXEC MyProc) AS TEMP
چنین چیزی توسط خود SQL Server پشتیبانی نمی‌شود. علت هم اینجا است که یک SP می‌تواند multiple result sets را بازگشت دهد؛ یعنی می‌تواند چندین select را با چندین ساختار متفاوت، بازگشت دهد و به همین جهت برای کار با Select مناسب نیست. مگر اینکه یک جدول موقتی را ایجاد کنید، سپس داده‌های خروجی رویه‌ی ذخیره شده را در آن ثبت کنید. دست آخر از آن جدول موقتی گزارش بگیرید. EF Core چنین کاری را برای شما انجام نمی‌دهد. بنابراین  FirstOrDefault در اینجا فقط یک client side evaluation خواهد بود که در نگارش 3 آن ممنوع شده‌است. اما می‌توانید client side evaluation را به صورت زیر بر روی FromSqlRaw فعال کنید:
context.Products.FromSqlRaw("[dbo].[Ten Most Expensive Products]").AsEnumerable().FirstOrDefault();
مطالب
کار با بانک‌های اطلاعاتی فاکس‌پرو و OleDB در دات نت 7
فرض کنید قصد خواندن اطلاعات یک بانک اطلاعاتی قدیمی فاکس‌پرو را با آخرین نگارش دات نت دارید. اگر سعی کنید از روش‌های و مطالب موجود استفاده کنید، هیچکدام جواب نخواهند داد! در این مطلب تغییرات صورت گرفته را بررسی می‌کنیم.


نیاز به درایور OleDB مخصوص بانک‌های اطلاعاتی قدیمی

برای کار با بانک‌های اطلاعاتی قدیمی از طریق ADO.NET، نیاز است بتوان به نحوی با آن‌ها ارتباط برقرار کرد و اینکار از طریق استاندارد OleDB که صرفا مختص به ویندوز است، قابل انجام است. برای مثال برای کار با فاکس‌پرو نیز در ابتدا باید درایور OleDB آن‌را نصب کرد که ... هیچکدام از لینک‌های قدیمی مایکروسافت در این زمینه دیگر وجود خارجی ندارند! آخرین نگارش مرتبط را می‌توانید در این آدرس و ذیل نام VFPOLEDBSetup.msi دریافت کنید (نگارش 9 را نصب می‌کند).


نیاز به دریافت بسته‌ی System.Data.OleDb

در اولین قدم جهت کار با درایور OleDB نصب شده، باید یک اتصال را توسط نمونه سازی شیء OleDbConnection ایجاد کرد که ... این شیء هم شناسایی نمی‌شود. به همین جهت باید بسته‌ی نیوگت مرتبط با آن‌را به صورت جداگانه‌ای دریافت و نصب کرد:
<ItemGroup>
   <PackageReference Include="System.Data.OleDb" Version="7.0.0"/>
</ItemGroup>


برنامه‌ی مبتنی بر درایور OleDB فاکس‌پرو اجرا نمی‌شود!

اولین سعی در برقراری ارتباط با درایور OleDB نصب شده، با خطای زیر خاتمه می‌یابد:
The 'VFPOLEDB' provider is not registered on the local machine.
مشکل اینجا است که درایور ارائه شده، 32 بیتی است و ما سعی داریم آن‌را در یک محیط 64 بیتی اجرا کنیم. به همین جهت خطای فوق ظاهر می‌شود. برای رفع آن باید PlatformTarget را به x86 تنظیم کرد:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
   <PlatformTarget>x86</PlatformTarget>
</PropertyGroup>
البته این تنظیم هم اگر پیشتر تنها runtime و یا SDK پیش‌فرض 64 بیتی را نصب کرده‌اید، سبب اجرای برنامه نخواهد شد. برای فعالسازی آن باید SDK x86 را هم جداگانه دریافت و نصب کنید.



روش یافتن نام پروایدر OleDB نصب شده

رشته‌های اتصالی OleDB، با =Provider، شروع می‌شوند. اکنون این سؤال مطرح می‌شود که پس از نصب VFPOLEDBSetup.ms یاد شده، دقیقا چه پروایدری به سیستم اضافه شده‌است و نام آن چیست؟
برای اینکار می‌توان از چندسطر زیر برای یافتن نام تمام پروایدرهای OleDB نصب شده‌ی بر روی سیستم استفاده کرد:
var oleDbEnumerator = new OleDbEnumerator();
using var elements = oleDbEnumerator.GetElements();
foreach (DataRow row in elements.Rows)
{
    Console.WriteLine($"{row["SOURCES_DESCRIPTION"]}: {row["SOURCES_NAME"]}");
}
که برای مثال یک خروجی آن می‌تواند به صورت زیر باشد:
Microsoft OLE DB Provider for SQL Server: SQLOLEDB
MSDataShape: MSDataShape
SQL Server Native Client 11.0: SQLNCLI11
Microsoft OLE DB Provider for Visual FoxPro: VFPOLEDB
OLE DB Provider for Microsoft Directory Services: ADsDSOObject
Microsoft OLE DB Driver for SQL Server: MSOLEDBSQL
Microsoft OLE DB Driver for SQL Server Enumerator: MSOLEDBSQL Enumerator
SQL Server Native Client 11.0 Enumerator: SQLNCLI11 Enumerator
Microsoft OLE DB Provider for Search: Windows Search Data Source
Microsoft OLE DB Provider for ODBC Drivers: MSDASQL
Microsoft OLE DB Enumerator for ODBC Drivers: MSDASQL Enumerator
Microsoft OLE DB Provider for Analysis Services 14.0: MSOLAP
Microsoft OLE DB Provider for Analysis Services 14.0: MSOLAP
Microsoft Jet 4.0 OLE DB Provider: Microsoft.Jet.OLEDB.4.0
Microsoft OLE DB Enumerator for SQL Server: SQLOLEDB Enumerator
Microsoft OLE DB Simple Provider: MSDAOSP
Microsoft OLE DB Provider for Oracle: MSDAORA
که در اینجا ما به دنبال یک سطر زیر هستیم:
Microsoft OLE DB Provider for Visual FoxPro: VFPOLEDB
یعنی نام دقیق پروایدر مرتبط با فاکس‌پرو، VFPOLEDB است.


روش خواندن اطلاعات یک بانک اطلاعاتی فاکس پرو

پس از این مقدمات و تنظیمات، اکنون می‌توانیم از قطعه کد متداول ADO.NET زیر، جهت خواندن اطلاعات یک بانک اطلاعاتی فاکس‌پرو، استفاده کنیم:
var connectionString = "Provider=VFPOLEDB;Data Source=" +
       @"C:\path\Db.DBF;Password=;Collating Sequence=MACHINE";
using var dbConnection = new OleDbConnection(connectionString);
using var dataAdapter = new OleDbDataAdapter("select family from Db.DBF", dbConnection);
using var dataset = new DataSet();
dataAdapter.Fill(dataset, "table1");

var sb = new StringBuilder();
foreach (DataRow dataRow in dataset.Tables[0].Rows)
{
    var iranSystem = dataRow[0] as string;
    var unicode = iranSystem.FromIranSystemToUnicode();
    if (!string.IsNullOrWhiteSpace(unicode))
    {
       sb.AppendLine($"[DataRow(\"{iranSystem}\", \"{unicode}\")]");
    }
}
توضیحات:
- نام پروایدر در رشته اتصالی، به VFPOLEDB تنظیم شده‌است.
- select انجام شده بر روی نام فایل dbf انجام می‌شود. یعنی هر فایل dbf، یک جدول را تشکیل می‌دهد.
- اگر نام فیلدهای موجود را نمی‌دانید، بجای select family از * select استفاده کنید و سپس بر روی DataSet پر شده، یک break-point را قرار دهید تا بتوانید نام تمام ستون‌ها را از آن استخراج کنید.
- رشته‌ای را که توسط درایور فاکس‌پرو دریافت می‌کنید، یک رشته‌ی اسکی سیستم عامل داس است که در دات نت، با encoding مساوی 1252 شناخته می‌شود. یعنی encoding این رشته‌ی دریافتی، یونیکد پیش‌فرض دات‌نت نیست و باید توسط متد Encoding.GetEncoding(1252).GetBytes پردازش شود. که در نگارش‌های جدید دات نت، این کد‌پیج‌ها به صورت پیش‌فرض مهیا نیستند و باید در ابتدا ثبت شوند تا قابل استفاده شوند:
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
- متد FromIranSystemToUnicode استفاده شده، جزئی از «DNTPersianUtils.Core» است که در صورت نیاز به تبدیل اطلاعات قدیمی ایران سیستمی به یونیکد، می‌تواند مورد استفاده قرار گیرد.
مطالب
آشنایی با Oslo - قسمت اول

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

دریافت Oslo

Oslo از سه قسمت عمده تشکیل شده است:
  • الف) زبان مدل سازی M
  • ب) ابزار مدل سازی Quadrant
  • ج) استفاده از SQL Server به عنوان مخزن

زبان مدل سازی M از سه قسمت به نام‌های MGraph ، MGrammer و MSchema تشکیل می‌شود.
MGrammer : گرامر مورد استفاده در SDL را تعریف می‌کند. Syntax Directed Translation
MSchema : طرح مدل را تعریف خواهد کرد.
MGraph : اگر MSchema بیانگر انواع باشد، MGraph بیانگر وهله‌ها خواهد بود.

یک مثال:
برنامه‌ی Intellipad را اجرا کنید (فرض بر این است که SDK فوق را نصب کرده‌اید)



در اینجا حالت را بر روی M Mode قرار دهید (مطابق تصویر) و همچنین از منوی ظاهر شده‌ی M Mode ، گزینه‌ی Generic T-SQL preview را هم انتخاب کنید.

اولین ماژول ما به صورت زیر است:

module Test1
{
type ApplicationUser
{
UserID : Integer64=AutoNumber();
FirstName :Text#15;
LastName : Text#25;
Password : Text#10;
} where identity UserID;
}

ابتدا نام ماژول مشخص می‌شود. شبیه به معرفی یک فضای نام در برگیرنده‌ی اشیای مربوطه. سپس type ، بیانگر همان MSchema خواهد بود.
در این مثال شناسه‌ی کاربری از نوع Integer64 خود افزایش یابنده تعریف شده است (نوع identity در اس کیوال سرور).
فیلدهای نام ، نام خانوادگی و کلمه‌ی عبور از نوع متنی با اندازه‌های مشخص 15 ، 25 و 10 کاراکتر تعریف شده‌اند. اگر اندازه مشخص نبود نوع را تنها Text تعریف کنید.
نکته:
1-اگر پس از Text علامت ? قرار گیرد، به معنای فیلدی از نوع nullable خواهد بود و برعکس. زیبایی Intellipad هم در اینجا است که بلافاصله پس از تایپ شما، عبارت T-SQL معادل را تولید می‌کند.
2-در اینجا UserID‌ به صورت identity معرفی شده است. در زبان ام ، identity همانند primary key‌ در عبارات T-SQL عمل می‌کند و نباید اشتباه گرفته شود.

تا اینجا فقط یک type تعریف شده است. برای تبدیل آن به یک جدول باید آن‌را توسعه داد.

ApplicationUserCollection : ApplicationUser*;

این سطر را به پس از تعریف type اضافه نمائید. علامت ستاره در اینجا به معنای صفر یا بیشتر است و جهت بسط نوع تعریف شده به یک مجموعه به کار می‌رود. اکنون با اضافه شدن این سطر، Intellipad بلافاصله عبارات T-SQL معادل را تولید خواهد کرد که در تصویر مشخص است. به این صورت MGraph ما که بیانگر وهله‌هایی از نوع ApplicationUser هستند تولید گردید.

اکنون قصد داریم گروهی از کاربرها را به صورت نمونه ایجاد کنیم:

ApplicationUserCollection
{
//using a named instance
User1 {
FirstName="user1",
LastName="name1",
Password="1@34"
},
User2 {
FirstName="user2",
LastName="name2",
Password="123@4"
},
User3 {
FirstName="user3",
LastName="name3",
Password="56#2"
},
User4 {
FirstName="user4",
LastName="name4",
Password="789@5"
}
}

سطرهای فوق را پس از تعریف ApplicationUserCollection در Intellipad اضافه کنید. بلافاصله Intellipad عبارات T-SQL معادل را برای ما تولید خواهد کرد.



ادامه دارد ...

مطالب دوره‌ها
کوئری نویسی مقدماتی در RavenDB
با شروع کوئری نویسی مقدماتی در RavenDB، در قسمت اول این مباحث، توسط فراخوانی متد Load یک سشن، آشنا شدید. در ادامه مباحث تکمیلی آن‌را مرور خواهیم کرد.

امکان استفاده از LINQ در RavenDB

RavenDB از LINQ جهت کوئری نویسی پشتیبانی می‌کند. برای استفاده از آن، در ادامه مطلب اول، ابتدا سرور RavenDB را اجرا نموده و سپس برنامه کنسول را به نحو ذیل تغییر دهید:
using System;
using System.Linq;
using Raven.Client.Document;
using RavenDBSample01.Models;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    var questions = session.Query<Question>().Where(x => x.Title.StartsWith("Raven"));
                    foreach (var question in questions)
                    {
                        Console.WriteLine(question.Title);
                    }
                }
            }
        }
    }
}
در RavenDB برای دسترسی به امکانات LINQ، کار با متد Query یک سشن آغاز می‌شود و پس از آن، امکان استفاده از متدهای متداول LINQ مانند مثال فوق وجود خواهد داشت. البته بدیهی است مباحثی مانند JOIN و امثال آن در یک بانک اطلاعاتی NoSQL پشتیبانی نمی‌شود. ضمنا باید درنظر داشت که مبحث safe by default در اینجا نیز اعمال می‌شود. برای مثال اگر به کنسول سرور RavenDB که در حال اجرا است مراجعه کنید، یک چنین خروجی را حین اجرای مثال فوق می‌توان مشاهده کرد که در آن pageSize پیش فرضی اعمال شده است:
Available commands: cls, reset, gc, q
Request #   1: GET     -   179 ms - <system>   - 404 - /docs/Raven/Replication/Destinations
Request #   2: GET     - 3,818 ms - <system>   - 200 - /indexes/dynamic/Questions?&query=Title%3ARaven*&pageSize=128
        Query: Title:Raven*
        Time: 3,494 ms
        Index: Auto/Questions/ByTitle
        Results: 2 returned out of 2 total.
یعنی در عمل کوئری‌را که اجرا کرده است، شبیه به کوئری ذیل می‌باشد و یک Take پیش فرض بر روی آن اعمال شده است:
var questions = session.Query<Question>().Where(x => x.Title.StartsWith("Raven")).Take(128);
علت این مساله نیز به تصمیم نویسنده اصلی آن بر می‌گردد؛ ایشان پیش از شروع به تهیه RavenDB، کار تهیه انواع و اقسام پروفایلرهای مهم ORMهای معروف مانند NHibernate و Entity framework را انجام داده است و در این حین، یکی از مهم‌ترین مشکلاتی را که با آن‌ها در کدهای متداول برنامه نویس‌ها یافته است، unbounded queries است. کوئری‌هایی که حد و مرزی برای بازگشت اطلاعات قائل نمی‌شوند. داشتن این نوع کوئری‌ها با تعداد بالای کاربر، یعنی مصرف بیش از حد RAM بر روی سرور، به همراه بار پردازشی بیش از حد و غیر ضروری. چون عملا حتی اگر 10 هزار رکورد بازگشت داده شوند، عموم برنامه نویس‌ها حداکثر 100 رکورد آن‌را در یک صفحه نمایش می‌دهند و نه تمام رکوردها را.


ارتباط Lucene.NET و RavenDB

کل LINQ API تهیه شده در RavenDB یک محصور کننده امکانات Lucene.NET است. اگر پیشتر با Lucene.NET کار کرده باشید، در خروجی حالت دیباگ کنسول سرور فوق، سطر «Query: Title:Raven*» آشنا به نظر خواهد رسید. دقیقا کوئری LINQ نوشته شده به یک کوئری با Syntax مخصوص Lucene.NET ترجمه شده‌است. برای نمونه اگر علاقمند باشید که مستقیما کوئری‌های خاص لوسین را در RavenDB اجرا کنید، از Syntax ذیل می‌توان استفاده کرد:
var questions = session.Advanced.LuceneQuery<Question>().Where("Title:Raven*").ToList();
و یا اگر علاقمند به حفظ کردن Syntax خاص لوسین نیستید، یک سری متد الحاقی خاص نیز در اینجا برای LuceneQuery تدارک دیده شده است. برای مثال کوئری رشته‌ای فوق، معادل کوئری strongly typed ذیل است:
var questions = session.Advanced.LuceneQuery<Question>().WhereStartsWith(x => x.Title, "Raven").ToList();

استفاده مجدد از کوئری‌ها در RavenDB

در RavenDB، متد Query به صورت immutable تعریف شده است و متد LuceneQuery حالت mutable دارد (ترکیبات آن نیز یک وهله است).
یک مثال:
var query = session.Query<User>().Where(x => x.Name.StartsWith("A"));
var ageQuery = query.Where(x => x.Age > 21);
var eyeQuery = query.Where(x => x.EyeColor == "blue");
در اینجا از کوئری ابتدایی، در دو کوئری مجزا استفاده مجدد شده است. ترجمه خروجی سه کوئری فوق به نحو زیر است:
query - Name:A*
ageQuery - (Name:A*) AND (Age_Range:{Ix21 TO NULL})
eyeQuery - (Name:A*) AND (EyeColor:blue)
به این معنا که زمانیکه به eyeQuery رسیدیم، نتیجه ageQuery با آن ترکیب نمی‌شود؛ چون متد Query از نوع immutable است.
در ادامه اگر همین سه کوئری فوق را با فرمت LuceneQuery تهیه کنیم، به عبارات ذیل خواهیم رسید:
var luceneQuery = session.Advanced.LuceneQuery<User>().WhereStartsWith(x => x.Name, "A");
var ageLuceneQuery = luceneQuery.WhereGreaterThan(x => x.Age, 21);
var eyeLuceneQuery = luceneQuery.WhereEquals(x => x.EyeColor, "blue");
در خروجی‌های این سه کوئری، مورد سوم مهم است:
luceneQuery - Name:A* 
ageLuceneQuery - Name:A* Age_Range:{Ix21 TO NULL}
eyeLuceneQuery - Name:A* Age_Range:{Ix21 TO NULL} EyeColor:blue
همانطور که مشاهده می‌کنید، کوئری سوم، عبارت کوئری دوم را نیز به همراه دارد؛ این مورد دقیقا مفهوم اشیاء mutable یا تک وهله‌ای است مانند LuceneQuery در اینجا.


And و Or شدن کوئری‌های ترکیبی در RavenDB
در مثال استفاده مجدد از کوئری‌ها، زمانیکه از Where استفاده شد، بین عبارات حاصل AND قرار گرفته است. این مورد را به نحو ذیل می‌توان تنظیم کرد و مثلا به OR تغییر داد:
session.Advanced.LuceneQuery<User>().UsingDefaultOperator(QueryOperator.And);

صفحه بندی اطلاعات در RavenDB

در ابتدای بحث عنوان شد که کوئری LINQ اجرا شده در RavenDB، یک Take مخفی و پیش فرض تنظیم شده به 128 آیتم را دارد. اکنون سؤال این خواهد بود که چگونه می‌توان اطلاعات را به صورت صفحه بندی شده، بر اساس شماره صفحه خاصی نمایش داد.
using System;
using System.Linq;
using Raven.Client.Document;
using RavenDBSample01.Models;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    int pageNumber = 0;
                    int resultsPerPage = 2;

                    var questions = session.Query<Question>()
                                           .Where(x => x.Title.StartsWith("Raven"))
                                           .Skip(pageNumber * resultsPerPage)
                                           .Take(resultsPerPage);
                    foreach (var question in questions)
                    {
                        Console.WriteLine(question.Title);
                    }
                }
            }
        }
    }
}
برای انجام صفحه بندی در RavenDB، کافی است از متدهای Skip و Take بر اساس محاسباتی که مشاهده می‌کنید، استفاده گردد.


دریافت اطلاعات آماری کوئری اجرا شده

در RavenDB امکان دریافت یک سری اطلاعات آماری از کوئری اجرا شده نیز وجود دارد؛ برای مثال یک کوئری چند ثانیه طول کشیده است، چه تعدادی رکورد را بازگشت داده است و امثال آن. برای پیاده سازی آن، نیاز است از متد الحاقی Statistics به نحو ذیل استفاده کرد:
using System;
using System.Linq;
using Raven.Client.Document;
using RavenDBSample01.Models;
using Raven.Client;

namespace RavenDBSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    int pageNumber = 0;
                    int resultsPerPage = 2;
                    RavenQueryStatistics stats;
                    var questions = session.Query<Question>()
                                           .Statistics(out stats)
                                           .Where(x => x.Title.StartsWith("Raven"))
                                           .Skip(pageNumber * resultsPerPage)
                                           .Take(resultsPerPage);
                    foreach (var question in questions)
                    {
                        Console.WriteLine(question.Title);
                    }

                    Console.WriteLine("TotalResults: {0}", stats.TotalResults);
                }
            }
        }
    }
}
متد الحاقی Statistics پس از متد Query که نقطه آغازین نوشتن کوئری‌های LINQ است، فراخوانی شده و یک پارامتر out از نوع RavenQueryStatistics تعریف شده در فضای نام Raven.Client را دریافت می‌کند. پس از پایان کوئری می‌توان از این خروجی جهت نمایش اطلاعات آماری کوئری استفاده کرد.


امکانات ویژه فضای نام Raven.Client.Linq

یک سری متد الحاقی خاص جهت تهیه ساده‌تر کوئری‌های LINQ در فضای نام Raven.Client.Linq قرار دارند که پس از تعریف آن قابل دسترسی خواهند بود:
var list = session.Query<Question>().Where(q => q.By.In<string>(arrayOfUsers))).ToArray()
برای مثال در اینجا متد الحاقی جدید In را مشاهده می‌کنید که شبیه به کوئری SQL ذیل در دنیای بانک‌های اطلاعاتی رابطه‌ای عمل می‌کند:
 SELECT * FROM tbl WHERE data IN (1, 2, 3)

اتصال به RavenDB با استفاده از برنامه معروف LINQPad

اگر علاقمند باشید که کوئری‌های خود را در محیط برنامه معروف LINQPad نیز آزمایش کنید، درایور مخصوص RavenDB آن‌را از آدرس ذیل می‌توانید دریافت نمائید:
مطالب
آشنایی با تابع PATINDEX در SQL Server
قبل از مطالعه باید بگویم سطح مقاله مبتدی می‌باشد.
گاهی اوقات در زمان Migration یک دیتابیس شما با جداولی برخورد می‌نمایید که محتویات بعضی از فیلد هایشان ترکیبی از عدد و حروف می‌باشد، و شما برای انجام یکسری از عملیات نیاز دارید. که حروف را از اعداد متمایز نمایید، یا اینکه مکان اولین کاراکتر غیر عددی را بعد از هر عدد، بیابید. برای انجام چنین کاری می‌توان از تابعی به نام Patindex استفاده نمود. 
تابع PATINDEX به شما امکان، مکان یابی یک یا چند حرف در بین رشته‌های متنی را می‌دهد.
Syntax تابع PATINDEX بصورت زیر میباشد:
PATINDEX ( '%pattern%' , expression )
تابع PATINDEX شامل دو آرگومان می‌باشد که هر کدام را به اختصار توضیح می‌دهیم:
1- آرگومان اولPattern نامگذاری شده است، Pattern در واقع یک الگوی اختصاصی میباشد که توسط کاربر، جهت جستجو در یک متن تعیین می‌شود. به بیان ساده‌تر اگر شما دنبال مکان حرف یا کلمه خاصی در یک رشته متنی می‌گردید، می‌بایست آن را در آرگومان Pattern  قرار دهید.
  • لازم است در آرگومان اول حداقل یک % وجود داشته باشد.
  • حداکثر تعداد کاراکترهایی را که می‌توان در آرگومان اول قرار داد 8000 میباشد.
2- آرگومان دوم یا همان Expression : متنی که عملیات جستجو روی آن اعمال می‌گردد، در این آرگومان قرار می‌گیرد.
  • اگر تعداد کارکترهای آرگومان دوم (Varchar(Max یا (nVarchar(Max  باشد، در آن صورت Type خروجی تابع PATINDEX از نوع bigint می‌باشد، در غیر این صورت Type خروجی تابع PATINDEX از نوع Int است.
  • اگر مقدار آرگومان دوم Null باشد، تابع PATINDEX مقدار Null بر می‌گرداند.
یک مثال کاربردی از تابع PATINDEX
یافتن اولین کاراکتر غیر عددی در رکودهای یک جدول
ابتدا در دیتابیس tempdb یک جدول به نام UsingPATINDEX مطابق Script زیر ایجاد،و چندین رکورد درون آن درج می‌نماییم:
USE tempdb
GO
CREATE TABLE UsingPATINDEX (ID INT, Words VARCHAR (100))
GO
INSERT INTO UsingPATINDEX (ID, Words)
SELECT 1, '1one'
UNION ALL
SELECT 2, '11eleven'
UNION ALL
SELECT 3, '2two'
UNION ALL
SELECT 4, '22twentytwo'
UNION ALL
SELECT 5, '111oneeleven'
GO
در ادامه QUERY زیر را اجرا نمایید:
SELECT PATINDEX('%[a-z]%',Words) 'مکان اولین کاراکتر غیر عددی',
SUBSTRING(Words,PATINDEX('%[a-z]%',Words),1) 'نام اولین کاراکتر غیر عددی بعد از عدد',
Words 'متن اصلی'
FROM  UsingPATINDEX
خروجی آن به شکل زیر است:

توضیح درباره QUERY :
قطعه کد زیر دنبال تمامی حروف a تا z ، درون فیلد Words می‌گردد و به اولین کاراکتر غیر عددی که می‌رسد، مکان آن را بر می‌گرداند.
PATINDEX('%[a-z]%',Words) 'مکان اولین کاراکتر غیر عددی'
قطعه کد زیر با توجه به مکان کاراکتر، خود کاراکتر را بر می‌گرداند:
SUBSTRING(Words,PATINDEX('%[a-z]%',Words),1) 'نام اولین کاراکتر غیر عددی بعد از عدد'

مثالی دیگر: فرض کنید،دنبال کلمه ای همانندensure  می گردید، بطوریکه دو حرف اول و دو حرف آخر آن را بخاطر می‌آورید و حرف میانی آن را بخاطر نمی‌آورید، در آن صورت  نیز می‌توانید از تابع PATINDEX استفاده نمایید، بدین شکل که به جای حرفی که بخاطر نمی‌آورید از _ استفاده کنید، همانند QUERY زیر:
SELECT PATINDEX('%en_ure%', 'please ensure the door is locked');
خروجی عدد 8 میباشد، که مکان حرف e است.
  • در تابع PATINDEX می‌توانید براساس Collation دلخواه عملیات جستجو را انجام دهید، برای روش‌تر شدن مطلب شکل زیر را مشاهده نمایید:

همانطور که درابتدای مطلبم گفتم در آرگومان اول می‌توان از یک % استفاده نمود، به مثال زیر توجه نمایید:
Select PATINDEX('a%', 'abc')
خروجی آن مقدار یک است.
مثالی دیگر:
Select PATINDEX('%a', 'cba')
خروجی آن مقدار 3 می‌باشد.
  • باید متذکر شوم، با دیدن دو مثال آخر،این تصور ایجاد نشود که تابع PATINDEX شبیه به تابع LIKE می‌باشد، چرا که تابع PATINDEX موقعیت کاراکتر را بر می‌گرداند، نه خود کاراکتر را،عملکرد تابع PATINDEX شبیه به عملکرد تابع CHARINDEX می‌باشد.




مطالب
معرفی و استفاده از DDL Triggers در SQL Server

استفاده از DDL Trigger

امکان ایجاد Trigger برای عملیات (DDL(Data Definition Language از SQL Server 2005  فراهم گردید. عملیاتی مانند ایجاد یک جدول جدید در بانک اطلاعاتی، اضافه شدن یک Login جدید و یا ایجاد یک بانک اطلاعاتی جدید را به وسیله این نوع Trigger‌ها می‌توان کنترل نمود. در حقیقت DDL Trigger به شما اجازه می‌دهد که از  تاثیر تعدادی از دستورات DDL جلوگیری کنید. بدین ترتیب که تقریباً هر دستور DDL به طور خودکار، تراکنشی (Transactional) اجرا می‌شود . می‌توان با دستور ROLLBACK TRANSACTION اجرای دستور DDL را لغو نمود. توجه شود همه دستورات DDL به صورت تراکنشی اجرا نمی‌شوند، به عنوان مثال دستور  ALTER DATABASE ممکن است Database را تغییر دهد. در این صورت ساختار فایلی Database را تغییر می‌دهد، از آنجائی که سیستم عامل ویندوز به صورت تراکنشی عمل نمی‌کند بنابراین شما نمی‌توانید این عمل فایل سیستمی را لغو نمائید. به هر حال شما می‌توانید Trigger را با ALTER DATABASE فعال (fire) کنید برای عملیات Auditing، ولی نمی‌توان از انجام عمل  ALTER  DATABASE جلوگیری کرد.
برای نمونه می‌خواهیم از حذف و یا تغییر جداول یک بانک اطلاعاتی که به صورت عملیاتی در حال سرویس دهی است جلوگیری کنیم، برای اینکار از دستورهای زیر استفاده می‌کنیم:
create trigger Prevent_AlterDrop
on database
for drop_table, alter_table
as
  print 'table can not be dropped or altered'
  rollback transaction
از عبارت  ON  برای مشخص کردن محدوده Trigger در سطح SQL Instance (در این صورت ON All SERVER نوشته می‌شود) و یا در سطح Database (در این حالت ON DATABASE نوشته می‌شود)  استفاده می‌شود و از عبارت FOR  برای مشخص کردن رویداد یا گروه رویدادی که سبب فراخوانی  Trigger می‌شود، استفاده  خواهد شد. 

1- معرفی تابع ()EVENTDATA

  این تابع، یک تابع سیستمی مهم است که در DDL Trigger استفاده می‌شود. در حالیکه DDL Trigger در هر سطحی فعال (fire) شود تابع سیستمی ()EVENTDATA فراخوانی (raise) می‌شود. خروجی تابع در قالب XML است. می‌توان اطلاعات را از تابع EVENTDATA دریافت کرد و آنها را در یک جدول با فیلدی از جنس XML و یا با استفاده از XPath Query  ثبت کرد (Logging). عناصر کلیدی (Key Elements) تابع EVENTDATA  به شرح زیر است:
•  EventType: نوع رویدادی که باعث فراخوانی Trigger شده است.
•  PostTime: زمانی که رویداد رخ می‌دهد.
•  SPID :SPID کاربری که باعث ایجاد رویداد شده است.
•  ServerName:  نام SQL Instance  که  رویداد در آن رخ داده است.
•  LoginName: نام Login که عمل مربوط به وقوع رویداد را اجرا می‌کند.
•  UserName: نام User که عمل مربوط به وقوع رویداد را اجرا می‌کند.
•  DatabaseName: نام Database که رویداد در آن رخ می‌دهد.
•  ObjectType: نوع Object که اصلاح، حذف و یا ایجاد شده است.
•  ObjectName: نام Object که اصلاح، حذف و یا ایجاد شده است.
•  TSQLCommand: دستور T-SQL که اجرا شده و باعث اجرا شدن Trigger شده است. 

2- بررسی یک سناریو نمونه

  برای نمونه در دستورات زیر جدولی با نام ddl_log
  CREATE TABLE ddl_log 
  (
     EventType nvarchar(100), 
 PostTime datetime,
 SPID nvarchar(100),
 ServerName nvarchar(100),
 LoginName nvarchar(100),
 UserName  nvarchar(100),
 DatabaseName  nvarchar(100),
 ObjectName  nvarchar(100),
 ObjectType  nvarchar(100),
 DefaultSchema nvarchar(100),
 [SID]  nvarchar(100),
 TSQLCommand nvarchar(2000));
و یک Trigger با نام log برای رویدادهایی که در سطح Database رخ می‌دهد، ایجاد می‌کنیم.
CREATE TRIGGER [Log] ON DATABASE
 FOR DDL_DATABASE_LEVEL_EVENTS
 AS
 DECLARE @data XML
 SET @data = EVENTDATA()
 INSERT INTO ddl_log
 VALUES (
   @data.value('(/EVENT_INSTANCE/EventType)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/PostTime)[1]',   'datetime'),
   @data.value('(/EVENT_INSTANCE/SPID)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ServerName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/LoginName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/UserName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/DatabaseName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ObjectName)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/ObjectType)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/DefaultSchema)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/SID)[1]',   'nvarchar(100)'),
   @data.value('(/EVENT_INSTANCE/TSQLCommand)[1]',   'nvarchar(2000)'));
نمونه ای از مقادیر ذخیره شده در جدول  ddl_log به شکل زیر خواهد بود:

3- ملاحظات

  در صورت فعال شدن  Trigger می‌توان برخی موارد مانند محدودیت زمانی، کاربر اجرا کننده و ... را  اضافه نمود. برای مثال در دستور زیر اجازه تغییرات در این زمان ( بین  7:00 A.M. تا .8:00 P.M ) امکان پذیر نیست و در صورت اقدام پیغام خطا دریافت می‌کنید و دستورات Create لغو خواهند شد و اگر خارج از زمان فوق دستورات DDL را اجرا کنید دستورات به طور موفقیت آمیز اجرا می‌شود و البته تغییرات نیز Log می‌شوند. 
 

  این Trigger تاثیرات کمی بر روی کارایی دارد به این دلیل که معمولاً رویداد‌های DDL به ندرت رخ می‌دهد. می‌توانید هنگامی که قصد دارید دستورات DDL را اجرا کنید موقتاً Trigger را با دستورات زیر غیر فعال نمائید:

پس از Overrdie کردن می‌توانید مجدداً Trigger را فعال کنید: 


4- معرفی DDL Event Groups: 

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

Server Level

DDL_SERVER_LEVEL_EVENTS

 DDL_LINKED_SERVER_EVENTS
 DDL_LINKED_SERVER_LOGIN_EVENTS
 DDL_REMOTE_SERVER_EVENTS
 DDL_EXTENDED_PROCEDURE_EVENTS
 DDL_MESSAGE_EVENTS
 DDL_ENDPOINT_EVENTS
 DDL_SERVER_SECURITY_EVENTS
 DDL_LOGIN_EVENTS
 DDL_GDR_SERVER_EVENTS
 DDL_AUTHORIZATION_SERVER_EVENT Database Level

Database Level

DDL_DATABASE_LEVEL_EVENTS  
DDL_TABLE_VIEW_EVENTS
DDL_TABLE-EVENTS
DDL_VIEW_EVENTS
DDL_INDEX_EVENTS
DDL_STATISTICS_EVENTS
DDL_DATABASE_SECURITY_EVENTS
DDL_CERTIFICATE_EVENTS
DDL_USER_EVENTS
DDL_ROLE_EVENTS
DDL_APPLICATION_ROLE_EVENTS
DDL_SCHEMA_EVENTS
DDL_GDR_DATABASE_EVENTS
DDL_AUTHORIZATION_DATABASE_EVENTS
DDL_FUNCTION_EVENTS
DDL_PROCEDUER_EVENTS
DDL_TRIGGER_EVENTS
DDL_PARTITION_EVENTS
DDL_PARTITION_FUNCTION_EVENTS
DDL_PARTITION_SCHEME_EVENTS
DDL_SSB_EVENTS
DDL_MESSAGE_TYPE_EVENTS
DDL_CONTRACT_EVENTS
DDL_QUEUE_EVENTS
DDL_SERVER_EVENTS
DDL_ROUTE_EVENTS
DDL_REMOTE_SERVICE_BINDING_EVENTS
DDL_XML_SCHEMA_COLEECTION_EVENTS
DDL_FULLTEXT_CATALOG_EVENTS
DDL_DEFAULT_EVENTS
DDL_EXTENDED_PROPERTY_EVENTS
DDL_PLAN_GUIDE_EVENTS
DDL_RULE_EVENTS
DDL_SYNONYM_EVENTS
DDL_EVENT_NOTIFICATION_EVENTS
DDL_ASSEMBLY_EVENTS
DDL_TYPE_EVENTS


مطالب
آموزش LINQ بخش پنجم
در ادامه سری آموزشی LINQ، موارد دیگری از عبارت‌های پرس و جو را بررسی می‌کنیم:
 • عبارت group
 • عبارت orderby
 • کلمات کلیدی ascending و descending
 • کلمه کلیدی by


بررسی عبارت group و کلمه‌ی کلیدی by

عبارت group یک توالی یک بعدی (flat sequence) یا ساده را از ورودی دریافت و یک توالی گروه بندی شده را تولید می‌کند.
مثال: در بخش دوم این سری آموزشی، کلاسی به نام Ingredient تعریف کردیم که مواد غذایی و کالری آنها را نگهداری می‌کرد. در این مثال قصد داریم مواد غذایی دریافت شده از توالی ورودی را بر اساس کالری آنها، به گروه‌هایی تقسیم کنیم، بطوریکه مواد غذایی با کالری‌های یکسان در یک گروه قرار بگیرند:
Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Lard", Calories=500},
   new Ingredient{Name = "Butter", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=100},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Oats", Calories=50}
};

IEnumerable<IGrouping<int, Ingredient>> query =
from i in ingredients
group i by i.Calories;

foreach (IGrouping<int, Ingredient> group in query)
{
   Console.WriteLine($"Ingredients with {group.Key} calories");
   foreach (Ingredient ingredient in group)
   {
      Console.WriteLine($"- { ingredient.Name}");
   }
}
در این مثال، از نوع جنریکی به نام IGrouping استفاده شده‌است که اولین پارامتر آن کلید گروه است (در این مثال کالری مواد غذایی) و دومین پارامتر آن، نوع لیست را مشخص می‌کند که در اینجا کلاس Ingredient است. اعضای لیست تولید شده، کالری یکسانی دارند. دلیل استفاده‌ی از نوع‌ها بصورت صریح و آشکار (Explicit) خوانایی بیشتر برای تشخیص خروجی‌های تولید شده‌ی در کد‌ها می‌باشد. ولی راه بهتر، جایگزین کردن کد <<IEnumerable<IGrouping<int, Ingredient با کلمه‌ی کلیدی var  می‌باشد.
خروجی مثال بالا:
 Ingredients with 500 calories
- Sugar
- Lard
- Butter
Ingredients with 100 calories
- Egg
- Milk
Ingredients with 50 calories
- Flour
- Oats

برای آشنایی بیشتر با اینترفیس IGrouping اینجا را مطالعه کنید.


عبارت orderby و کلمات کلیدی ascending  و descending 

عبارت orderby برای تولید یک توالی مرتب شده‌ی بصورت صعودی (ascending)  و یا نزولی (descending) مورد استفاده قرار می‌گیرد.
مثال: توالی مثال قبل را در نظر بگیرد:
IOrderedEnumerable<Ingredient> sortedByNameQuery =
from i in ingredients
orderby i.Name
select i;

foreach (var ingredient in sortedByNameQuery)
{
   Console.WriteLine(ingredient.Name);
}
خروجی مثال بالا:
Butter
Egg
Flour
Lard
Milk
Oats
Sugar
علملیات مرتب سازی بصورت پیش فرض بصورت صعودی می‌باشد (Ascending). برای تغییر این حالت می‌توانید از کلمه‌ی کلیدی descending استفاده کنید:
IOrderedEnumerable<Ingredient> sortedByNameQuery =
from i in ingredients
orderby i.Name descending
select i;
با اجرای مجدد برنامه، خروجی زیر را مشاهده خواهید کرد:
 Sugar
Oats
Milk
Lard
Flour
Egg
Butter
عبارت orderby  را می‌توان با عبارت groupby ترکیب کرد و نتیجه‌ی حاصل از مثال اول این مطلب را بصورت مرتب شده بر اساس کالری مواد غذایی مشاهده کرد.
مثال:
 Ingredient[] ingredients =
{
   new Ingredient{Name = "Sugar", Calories=500},
   new Ingredient{Name = "Lard", Calories=500},
   new Ingredient{Name = "Butter", Calories=500},
   new Ingredient{Name = "Egg", Calories=100},
   new Ingredient{Name = "Milk", Calories=100},
   new Ingredient{Name = "Flour", Calories=50},
   new Ingredient{Name = "Oats", Calories=50}
};

IEnumerable<IGrouping<int, Ingredient>> query =
from i in ingredients
group i by i.Calories
into calorieGroup
orderby calorieGroup.Key
select calorieGroup;

foreach (IGrouping<int, Ingredient> group in query)
{
   Console.WriteLine($"Ingredients with {group.Key} calories");
   foreach (Ingredient ingredient in group)
   {
     Console.WriteLine($"- { ingredient.Name}");
   }
}
خروجی مثال بالا :
 Ingredients with 50 calories
- Flour
- Oats
Ingredients with 100 calories
- Egg
- Milk
Ingredients with 500 calories
- Sugar
- Lard
- Butter
همانطور که مشاهده می‌کنید بر عکس حالت قبلی که خروجی گروه‌ها بصورت نزولی بود، اینجا خروجی بر اساس کالری غذا و بصورت صعودی، نمایش داده شده است.


در این سری آموزشی با دو روش نوشتن پرس و جو، در LINQ آشنا شدیم:
 1- Fluent Style (استفاده از متدهای الحاقی برای انجام عملیات‌های مختلف بر روی توالی)
 2- Expression Style (استفاده از کلمات کلیدی (key word) برای انجام عملیات‌های مختلف بر روی توالی)

 هر یک از روش‌های فوق مزایایی دارند که با توجه به شرایطی که با آن روبرو هستیم از آنها استفاده می‌کنیم:
 • تعداد عملگر‌ها : در صورتی که پرس و جو نیاز به یک عملگر بر روی توالی داشته باشد می‌توان از روش اول استفاده کرد. به این خاطر که نسبت به روش دوم تعداد دستورات کمتری مورد استفاده قرار خواهد گرفت.
مثال :
 var q1 = ingredients.Where(x => x.Calories > 100);
var q2 = from i in ingredients
 where i.Calories >
100 select i;
در مثال فوق نتیجه‌ی اجرای دو پرس و جو، موادی است که کالری آنها بیشتر از 100 می‌باشد. همانطور که مشاهده می‌کنید روش اول مختصرتر از روش دوم است. این مورد در زمانی است که تنها یک عملیات بر روی توالی اجرا شود.
 • پرس و جویی که قرار است نوشته شود، از نظر عملیات بر روی توالی ساده باشد: در این حالت روش استفاده شده تفاوتی نمی‌کند و بستگی به روش برگزیده و مورد علاقه‌ی برنامه نویس و یا تیم برنامه نویسی دارد. مثلا زمانیکه تنها عملگر‌های Where  و orderby بخواهند اجرا شود.
 • پرس و جوهایی که با متغیر‌های range مختلفی رو برو هستند: در این حالت استفاده از عبارت‌های پرس و جو راحت از روش عملگر‌های پرس و جو می‌باشد.

لیستی از عملگرهای جستجو که در روش عبارت‌های جستجو معادل آنها مهیا شده است :
 • GroupBy
 • GroupJoin
 • Join
 • OrderBy
 • OrderByDescending
 • Select
 • SelectMany
 • ThenBy
 • ThenByDescending
 • Where

در بسیاری از پرس و جو‌ها می‌توانیم هر دو روش را با هم ترکیب کنیم که نمونه‌ای از آن، در جلسه‌ی چهارم در زمان استفاده‌ی از متد DefaultIfEmpty استفاده شد. درمثال زیر استفاده از عملگر count، با استفاده از روش اول، به همراه عبارت پرس و جوی تولید شده‌ی با روش دوم، نمایش داده شده است:
 int mixedQuery =
(from i in ingredients
where i.Calories > 100
select i).Count();
مطالب
استفاده از Sparse Columns در SQL Server 2012
مقدمه
مقدار null به معنی پوچ و هیچ می‌باشد اما زمانی که در مقدار دهی جداول از آن استفاده می‌نمایم با توجه به نوع آن ستون فضای متفاوتی اشتغال می‌نماید. شاید در پایگاه داده‌های کوچک زیاد مطرح نباشد اما زمانی که حداقل چند گیگ حجم آن باشد و فرضا 20 تا 30 درصد آن از مقادیر null پر شده باشد فضای زیای از پوچ گرفته شده است این در حالی است که خیلی از توسعه دهندگان اصلا به اهمیت استفاده از null توجهی نمی‌کنند و از مقادیری غیر معتبری مثل 0 یا 1- در آن ستون به جای null استفاده می‌کنند.
SQL Server Sparce Columns
sparse column یا ستون‌های تنک قابلیتی از که از SQL Server 2008 اضافه شده و به ستون‌های عادی امکان استفاده بهینه از فضای ذخیره شده برای مقادیر null را می‌دهد. در واقع sparse column فضای مورد نیاز برای مقادیر null نسبت به مقادیر غیر null را کاهش می‌دهد. با استفاده از sparse column فضای ذخیره شده حداقل 20 تا 40 درصد کمتر خواهد شد.

ویژگی‌های Sparse Columns
  • SQL Server Database Engine از کلمه کلیدی SPARSE برای تعریف یک ستون که مقادیر آن می‌بایست بهینه شود استفاده می‌نماید.
  • نمای Catalog  جداول با ستون sparse شبیه جداول معمولی می‌باشد.
  • مقدار برگشتی از تابع COLUMNS_UPDATED با ستون sparce متفاوت از ستون معمولی است.
در نوع داده‌های زیر امکان استفاده از sparce columns  را ندارند:
 geography  text
 geometry   timestamp 
 image   user-defined data types 
ntext  
sparse column فضای بیشتری برای ذخیره داده‌های غیر null نسبت به داده‌های نشانه گذاری نشده با SPARSE لازم دارد و این فضا4 بایت بیشتر از ستون معمولی است. برآورد فضای ذخیره شده براساس نوع داده با طول ثابت در جدول زیر آورده شده است:
 نوع داده  بایت بدون sparse  بایت sparse  درصد null
 bit   0.125   5  98%
 tinyint   1   5  86%
 smallint   2  6  76%
 int   4  8  64%
bigint  8  12  52%
 real   4  8  64%
 float   8  12  52%
 smallmoney   4  8  64%
 money   8  12  52%
 smalldatetime   4  8  64%
 datetime   8  12  52%
 uniqueidentifier   16  20  43%
 date   3  7  69%
نوع داده با دقت - وابسته به طول
 
 نوع داده  بایت بدون sparse  یابت sparse  درصد null 
 (datetime(2   6  10  57%
 (datetime(2   8  12  52%
 (time(0   3  7  69%
 (time(7   5  9  60%
 (datetimetoffset(0   8  12  52%
 (datetimetoffset (7   10  14  49%
 (decimal/numeric(1,s   5  9  60%
 (decimal/numeric(38,s   17  21  42%
 (vardecimal(p,s      
نوع داده - داده وابسته به طول
نوع داده 
بایت بدون sparse   یابت sparse  درصد null
 sql_variant   2*   2*   60% 
 varchar or char   2*  4*+   60% 
 nvarchar or nchar   2*   4*   60% 
 varbinary or binary   2*   4*   60% 
 xml   2*   4*   60% 
 hierarchyid   2*  4*   60% 

محدویت‌های استفاده از Sparse columns
  • sparse column می‌ بایست nullable باشد و نمی‌تواند ROWGUIDCOL یا IDENTITY باشد. 
  • sparse column مقدار پیش فرض نمی‌تواند داشته باشد 
  • ستون محاسبه ای نمی‌تواند sparse باشد
  • sparse column نمی‌تواند بخشی از clustered index یا  unique primary key index باشد
  • sparse column  نمی تواند بخشی از  user-defined table باشد

مثالی از کاربرد Sparse columns
CREATE TABLE Employees_sparse (
   EMP_ID INT IDENTITY(5001,1) PRIMARY KEY, 
   SSN CHAR(9) NOT NULL, 
   TITLE CHAR(10) SPARSE NULL, 
   FIRSTNAME VARCHAR(50) NOT NULL, 
   MIDDLEINIT CHAR(1) SPARSE NULL, 
   LASTNAME VARCHAR(50) NOT NULL, 
   EMAIL CHAR(50) SPARSE NULL)
GO
CREATE TABLE Employees (
   EMP_ID INT IDENTITY(5001,1) PRIMARY KEY, 
   SSN CHAR(9) NOT NULL, 
   TITLE CHAR(10) NULL, 
   FIRSTNAME VARCHAR(50) NOT NULL, 
   MIDDLEINIT CHAR(1) NULL, 
   LASTNAME VARCHAR(50) NOT NULL, 
   EMAIL CHAR(50) NULL)
GO
در این دو جدول یکی با سه ستون Sparse  و دیگری بدون Sparse ایجاد شده و با  50000 ردیف داده پر شده است حال با رویه ذخیره شده sp_spaceused می‌توان فضای ذخیره شده دو جدول را باهم مقایسه نمود.
sp_spaceused 'Employees'
GO
sp_spaceused 'Employees_sparse' 

البته ذکر این نکته گفتی است که بهتر است از این تکنیک برای جداولی که تعداد زیادی ستون null دارند استفاده شود. 
مطالب
پیاده سازی SoftDelete در EF Core
در مورد حذف منطقی در EF 6x، پیشتر مطالبی را در این سایت مطالعه کرده‌اید:
- «پیاده سازی حذف منطقی در Entity framework» حذف منطقی، یکی از الگوهای بسیار پرکاربرد در برنامه‌های تجاری است. توسط آن بجای حذف فیزیکی اطلاعات، آن‌ها را تنها به عنوان رکوردی حذف شده، «علامتگذاری» می‌کنیم. مزایای آن نیز به شرح زیر هستند:
- داشتن سابقه‌ی حذف اطلاعات
- جلوگیری از cascade delete
- امکان بازیابی رکوردها و امکان ایجاد قسمتی به نام recycle bin در برنامه (شبیه به recycle bin در ویندوز که امکان بازیابی موارد حذف شده را می‌دهد)
- امکان داشتن رکوردهایی که در یک برنامه (به ظاهر) حذف شده‌اند، اما هنوز در برنامه‌ی دیگری در حال استفاده هستند.
- بالابردن میزان امنیت برنامه. فرض کنید سایت شما هک شده و شخصی، دسترسی به پنل مدیریتی و سطوح دسترسی مدیریتی برنامه را پیدا کرده‌است. در این حالت حذف تمام رکوردهای سایت توسط او، تنها به معنای تغییر یک بیت، از یک به صفر است و بازگرداندن این درجه از خسارت، تنها با روشن کردن این بیت، برطرف می‌شود.

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


نیاز به تعریف دو خاصیت جدید در هر جدول

هر جدولی که قرار است soft delete به آن اعمال شود، باید دارای دو فیلد جدید bool IsDeleted و DateTime? DeletedAt باشد. می‌توان این خواص را به هر موجودیتی به صورت دستی اضافه کرد و یا می‌توان ابتدا یک کلاس پایه‌ی abstract را برای آن ایجاد کرد:
using System;

namespace EFCoreSoftDelete.Entities
{
    public abstract class BaseEntity
    {
        public int Id { get; set; }


        public bool IsDeleted { set; get; }
        public DateTime? DeletedAt { set; get; }
    }
}
و سپس موجودیت‌هایی را که قرار است از soft delete پشتیبانی کنند، توسط آن علامتگذاری کرد؛ مانند موجودیت Blog:
using System.Collections.Generic;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Blog : BaseEntity
    {
        public string Name { set; get; }

        public virtual ICollection<Post> Posts { set; get; }
    }

    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            builder.Property(blog => blog.Name).HasMaxLength(450).IsRequired();
            builder.HasIndex(blog => blog.Name).IsUnique();

            builder.HasData(new Blog { Id = 1, Name = "Blog 1" });
            builder.HasData(new Blog { Id = 2, Name = "Blog 2" });
            builder.HasData(new Blog { Id = 3, Name = "Blog 3" });
        }
    }
}
که هر بلاگ از تعدادی مطلب تشکیل شده‌است:
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace EFCoreSoftDelete.Entities
{
    public class Post : BaseEntity
    {
        public string Title { set; get; }

        public Blog Blog { set; get; }
        public int BlogId { set; get; }
    }

    public class PostConfiguration : IEntityTypeConfiguration<Post>
    {
        public void Configure(EntityTypeBuilder<Post> builder)
        {
            builder.Property(post => post.Title).HasMaxLength(450);
            builder.HasOne(post => post.Blog).WithMany(blog => blog.Posts).HasForeignKey(post => post.BlogId);

            builder.HasData(new Post { Id = 1, BlogId = 1, Title = "Post 1" });
            builder.HasData(new Post { Id = 2, BlogId = 1, Title = "Post 2" });
            builder.HasData(new Post { Id = 3, BlogId = 1, Title = "Post 3" });
            builder.HasData(new Post { Id = 4, BlogId = 1, Title = "Post 4" });
            builder.HasData(new Post { Id = 5, BlogId = 2, Title = "Post 5" });
        }
    }
}
مزیت علامتگذاری این کلاس‌ها، امکان کوئری گرفتن از آن‌ها نیز می‌باشد که در ادامه از آن استفاده خواهیم کرد.


حذف خودکار رکوردهایی که Soft Delete شده‌اند، از نتیجه‌ی کوئری‌ها و گزارشات

تا اینجا فقط دو خاصیت ساده را به کلاس‌های مدنظر خود اضافه کرده‌ایم. پس از آن یا می‌توان در هر جائی برای مثال شرط context.Blogs.Where(blog => !blog.IsDeleted) را به صورت دستی اعمال کرد و در گزارشات، رکوردهای حذف منطقی شده را نمایش نداد و یا از زمان ارائه‌ی EF Core 2x می‌توان برای آن‌ها Query Filter تعریف کرد. برای مثال می‌توان به تنظیمات موجودیت Blog و یا Post مراجعه نمود و با استفاده از متد HasQueryFilter، همان شرط blog => !blog.IsDeleted را به صورت سراسری به تمام کوئری‌های مرتبط با این موجودیت‌ها اعمال کرد:
    public class BlogConfiguration : IEntityTypeConfiguration<Blog>
    {
        public void Configure(EntityTypeBuilder<Blog> builder)
        {
            // ...
            builder.HasQueryFilter(blog => !blog.IsDeleted);
        }
    }
از این پس ذکر context.Blogs دقیقا معنای context.Blogs.Where(blog => !blog.IsDeleted) را می‌دهد و دیگر نیازی به ذکر صریح شرط متناظر با soft delete نیست.
در این حالت کوئری‌های نهایی به صورت خودکار دارای شرط زیر خواهند شد:
SELECT [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[IsDeleted] <> CAST(1 AS bit)


اعمال خودکار QueryFilter مخصوص Soft Delete به تمام موجودیت‌ها

همانطور که عنوان شد، مزیت علامتگذاری موجودیت‌ها با کلاس پایه‌ی BaseEntity، امکان کوئری گرفتن از آن‌ها است:
namespace EFCoreSoftDelete.DataLayer
{
    public static class GlobalFiltersManager
    {
        public static void ApplySoftDeleteQueryFilters(this ModelBuilder modelBuilder)
        {
            foreach (var entityType in modelBuilder.Model
                                                    .GetEntityTypes()
                                                    .Where(eType => typeof(BaseEntity).IsAssignableFrom(eType.ClrType)))
            {
                entityType.addSoftDeleteQueryFilter();
            }
        }

        private static void addSoftDeleteQueryFilter(this IMutableEntityType entityData)
        {
            var methodToCall = typeof(GlobalFiltersManager)
                                .GetMethod(nameof(getSoftDeleteFilter), BindingFlags.NonPublic | BindingFlags.Static)
                                .MakeGenericMethod(entityData.ClrType);
            var filter = methodToCall.Invoke(null, new object[] { });
            entityData.SetQueryFilter((LambdaExpression)filter);
        }

        private static LambdaExpression getSoftDeleteFilter<TEntity>() where TEntity : BaseEntity
        {
            return (Expression<Func<TEntity, bool>>)(entity => !entity.IsDeleted);
        }
    }
}
در اینجا در ابتدا تمام موجودیت‌هایی که از BaseEntity ارث بری کرده‌اند، یافت می‌شوند. سپس بر روی آن‌ها قرار است متد SetQueryFilter فراخوانی شود. این متد بر اساس تعاریف EF Core، یک LambdaExpression کلی را قبول می‌کند که نمونه‌ی آن در متد getSoftDeleteFilter تعریف شده و سپس توسط متد addSoftDeleteQueryFilter به صورت پویا به modelBuilder اعمال می‌شود.

محل اعمال آن نیز در انتهای متد OnModelCreating است تا به صورت خودکار به تمام موجودیت‌های موجود اعمال شود:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {

        //...


        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            modelBuilder.ApplyConfigurationsFromAssembly(typeof(BaseEntity).Assembly);
            modelBuilder.ApplySoftDeleteQueryFilters();
        }


مشکل! هنوز هم حذف فیزیکی رخ می‌دهد!

تنظیمات فوق، تنها بر روی کوئری‌های نوشته شده تاثیر دارند؛ اما هیچگونه تاثیری را بر روی متد Remove و سپس SaveChanges نداشته و در این حالت، هنوز هم حذف واقعی و فیزیکی رخ می‌دهد.
 برای رفع این مشکل باید به EF Core گفت، هر چند دستور حذف صادر شده، اما آن‌را تبدیل به دستور Update کن؛ یعنی فیلد IsDelete را به 1 و فیلد DeletedAt را با زمان جاری مقدار دهی کن:
namespace EFCoreSoftDelete.DataLayer
{
    public static class AuditableEntitiesManager
    {
        public static void SetAuditableEntityOnBeforeSaveChanges(this ApplicationDbContext context)
        {
            var now = DateTime.UtcNow;

            foreach (var entry in context.ChangeTracker.Entries<BaseEntity>())
            {
                switch (entry.State)
                {
                    case EntityState.Added:
                        //TODO: ...
                        break;
                    case EntityState.Modified:
                        //TODO: ...
                        break;
                    case EntityState.Deleted:
                        entry.State = EntityState.Unchanged; //NOTE: For soft-deletes to work with the original `Remove` method.

                        entry.Entity.IsDeleted = true;
                        entry.Entity.DeletedAt = now;
                        break;
                }
            }
        }
    }
}
در اینجا با استفاده از سیستم tracking، رکوردهای حذف شده‌ی با وضعیت EntityState.Deleted، به وضعیت EntityState.Unchanged تغییر پیدا می‌کنند، تا دیگر حذف نشوند. اما در ادامه چون دو خاصیت IsDeleted و DeletedAt این موجودیت، ویرایش می‌شوند، وضعیت جدید Modified خواهد بود که به کوئری‌های Update تفسیر می‌شوند. به این ترتیب می‌توان همانند قبل یک رکورد را حذف کرد:
var post1 = context.Posts.Find(1);
if (post1 != null)
{
   context.Remove(post1);

   context.SaveChanges();
}
اما دستوری که توسط EF Core صادر می‌شود، یک Update است:
Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:11:32' (Nullable = true), @p1='True'], CommandType='Text', CommandTimeout='30']
SET NOCOUNT ON;
UPDATE [Posts] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

محل اعمال متد SetAuditableEntityOnBeforeSaveChanges فوق، پیش از فراخوانی SaveChanges و به صورت زیر است:
namespace EFCoreSoftDelete.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        // ...

        public override int SaveChanges(bool acceptAllChangesOnSuccess)
        {
            ChangeTracker.DetectChanges();

            beforeSaveTriggers();

            ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges(acceptAllChangesOnSuccess);

            ChangeTracker.AutoDetectChangesEnabled = true;
            return result;
        }

        // ...

        private void beforeSaveTriggers()
        {
            setAuditProperties();
        }

        private void setAuditProperties()
        {
            this.SetAuditableEntityOnBeforeSaveChanges();
        }
    }
}


مشکل! رکوردهای وابسته حذف نمی‌شوند!

حالت پیش‌فرض حذف رکوردها در EFCore به cascade delete تنظیم شده‌است. یعنی اگر blog با id=1 حذف شود، نه فقط این blog، بلکه تمام مطالب وابسته‌ی به آن نیز حذف خواهند شد. اما در اینجا اگر این بلاگ را حذف کنیم:
 ar blog1 = context.Blogs.FirstOrDefault(blog => blog.Id == 1);
if (blog1 != null)
{
   context.Remove(blog1);

   context.SaveChanges();
}
تنها تک رکورد متناظر با آن حذف منطقی شده و مطالب متناظر با آن خیر. برای رفع این مشکل باید به صورت زیر عمل کرد:
var blog1AndItsRelatedPosts = context.Blogs
    .Include(blog => blog.Posts)
    .FirstOrDefault(blog => blog.Id == 1);
if (blog1AndItsRelatedPosts != null)
{
    context.Remove(blog1AndItsRelatedPosts);

    context.SaveChanges();
}
ابتدا باید رکوردهای وابسته را توسط یک Include به حافظه وارد کرد و سپس دستور Delete را بر روی کل آن صادر نمود که یک چنین خروجی را تولید می‌کند:
SELECT [t].[Id], [t].[DeletedAt], [t].[IsDeleted], [t].[Name], [t0].[Id], [t0].[BlogId], [t0].[DeletedAt], [t0].[IsDeleted], [t0].[Title]
FROM (
SELECT TOP(1) [b].[Id], [b].[DeletedAt], [b].[IsDeleted], [b].[Name]
FROM [Blogs] AS [b]
WHERE ([b].[IsDeleted] <> CAST(1 AS bit)) AND ([b].[Id] = 1)
) AS [t]
LEFT JOIN (
SELECT [p].[Id], [p].[BlogId], [p].[DeletedAt], [p].[IsDeleted], [p].[Title]
FROM [Posts] AS [p]
WHERE [p].[IsDeleted] <> CAST(1 AS bit)
) AS [t0] ON [t].[Id] = [t0].[BlogId]
ORDER BY [t].[Id], [t0].[Id]

Executing DbCommand [Parameters=[@p2='1', @p0='2020-09-17T05:25:00' (Nullable = true), @p1='True',
 @p5='2', @p3='2020-09-17T05:25:00' (Nullable = true), @p4='True', @p8='3',
@p6='2020-09-17T05:25:00' (Nullable = true), @p7='True',
 @p11='4', @p9='2020-09-17T05:25:00' (Nullable = true), @p10='True'], CommandType='Text', CommandTimeout='30']

SET NOCOUNT ON;
UPDATE [Blogs] SET [DeletedAt] = @p0, [IsDeleted] = @p1
WHERE [Id] = @p2;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p3, [IsDeleted] = @p4
WHERE [Id] = @p5;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p6, [IsDeleted] = @p7
WHERE [Id] = @p8;
SELECT @@ROWCOUNT;

UPDATE [Posts] SET [DeletedAt] = @p9, [IsDeleted] = @p10
WHERE [Id] = @p11;
SELECT @@ROWCOUNT;
ابتدا اولین بلاگ را حذف منطقی کرده؛ سپس تمام مطالب متناظر با آن‌را که پیشتر حذف منطقی نشده‌اند، یکی یکی به صورت حذف شده، علامتگذاری می‌کند. به این ترتیب cascade delete منطقی نیز در اینجا میسر می‌شود.


یک نکته: مشکل حذف منطقی و رکوردهای منحصربفرد

فرض کنید در جدولی، فیلد نام کاربری را به عنوان یک فیلد منحصربفرد تعریف کرده‌اید و اکنون رکوردی در این بین، حذف منطقی شده‌است. مشکلی که در آینده بروز خواهد کرد، عدم امکان ثبت رکورد جدیدی با همان نام کاربری است که حذف منطقی شده‌است؛ چون یک unique index بر روی آن وجود دارد. در این حالت اگر از SQL Server استفاده می‌کنید، از قابلیتی به نام filtered indexes پشتیبانی می‌کند که در آن امکان تعریف یک شرط و predicate، در حین تعریف ایندکس‌ها وجود دارد. در این حالت می‌توان رکوردهای حذف منطقی شده را به ایندکس وارد نکرد.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: EFCoreSoftDelete.zip