پاسخ به بازخورد‌های پروژه‌ها
توضیح گام های اجرا شده در پروژه
سلام؛
هدف از انجام این پروژه برای من چسباندن قطعات مختلف یک پازل به هم بودند تا بتوان به یک تصویر خوب رسید.منظور من این است که entity framework و ASP.NET MVC و bootstrap و best practice‌های آن‌ها به تنهایی و جدا از هم به نظر ساده و راحت و خوب بیایند، اما درگیر شدن همه‌ی آن‌ها در یک پروژه‌ی واقعی، واقعا چالش بر انگیز است.
من دانشجو هستم و تقریبا استارت این پروژه را از آبان ماه زدم، اما به دلیل یک سری مشکلات از جمله همین دانشجو بودن، کار به کندی پیش رفت و حتی وقفه‌های چند ماهه در آن پیش اومد. هدف من این بود که اساسا یک سیستم با کیفیت بنویسم و در ابتدای کار هم، کار به خوبی پیش می‌رفت، اما با توجه به مشکلات ذکر شده، عمده کار کدنویسی در تعطیلات عید نوروز صورت گرفت، و کاملا از کدنویسی انجام شده مشهود است ک ههمان قسمت هایی که در عید نوروز کدنویسی شده اند، اصطلاحا سرهم بندی شده اند( به خصوص در کدهای سمت کلاینت)
در مورد گام‌های انجام شده؛ پروژه به این منوال انجام شد:
- تحلیل ساختار بانک اطلاعاتی مورد نیاز
- شروع به تحقیق در مورد امکانات مورد نیاز 
- دعوت همکای برای کار گروهی توسط دوستان ( کسی قبول نکرد البته دی:)
- با توجه به محدودیت‌های یافت شده در تحقیقات، ساختار بانک اطلاعاتی نهایی می‌شود.
- انتخاب فریم ورک‌های مناسب( که در اینجا Entity Framework برای orm و ASP.NET MVC برای کدنویسی سمت سرور و bootstrap برای css و jquery هم برای جاوا اسکریپت)
- تحقیق در مورد best practice‌های موجود در مورد هر یک از فریم ورک‌های فوق
-شروع کدنویسی
در مورد قسمت مدیریت کاربران، هدف طراحی یک سیستم خیلی منعطف بود که قطعا با memebrship خود دات نت امکان پذیر نبود. متاسفانه به دلیل مشکلات پیش اومده این قسمت از پروژه هم سرهم بندی کردم و به یک سیستم ساده اکتفا کردم.
برای پیاده سازی آن هم شما کافیست در گوگل عبارت implement custom membership in asp.net mvc را سرچ کنید. مطمئن باشید کلی مطلب پیدا خوهید کرد که با جمع بندی آن یک سیستم خوب می‌توانید پیاده سازی کنید.
الان همین سیستم پیاده سازی شده در سایت یک باگ دارد که بعد از مدتی remember me آ از کار می‌افتد.کوکی کاربر اعتبار دارد، اما رویداد متناظر آن برای اعتبار سنجی اتفاق نمی‌افتد!
الان هم در حال تحقیق برای پیاده سازی یک سیستم اعتبارسنجی  کامل‌تر و اصولی‌تر و یک پارچه‌تر با ASP.NET MVC  هستم که مقاله‌ی زیر خیلی به من کمک کرد.(امیدوارم برای شما هم مفید باشد)
الان هم برنامه ای برای ارتقا این سیستم دارم و مهمترین تغییر آن را می‌توان به استفاده از angularjs برای نوشتن بخش مدیریتی و پیاده سازی آن به صورت single page دانست.( البته اگر این کمردرد بزاره دی:)
امیدوارم دوستان با بازخوردهای خوب خودشون، در ارتقای سطح کیفی کار کمک کنند.
مطالب
الگویی برای مدیریت دسترسی همزمان به ConcurrentDictionary
ConcurrentDictionary، ساختار داده‌ای است که امکان افزودن، دریافت و حذف عناصری را به آن به صورت thread-safe میسر می‌کند. اگر در برنامه‌ای نیاز به کار با یک دیکشنری توسط چندین thread وجود داشته باشد، ConcurrentDictionary راه‌حل مناسبی برای آن است.
اکثر متدهای این کلاس thread-safe طراحی شده‌اند؛ اما با یک استثناء: متد GetOrAdd آن thread-safe نیست:
 TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory);


بررسی نحوه‌ی کار با متد GetOrAdd

این متد یک کلید را دریافت کرده و سپس بررسی می‌کند که آیا این کلید در مجموعه‌ی جاری وجود دارد یا خیر؟ اگر کلید وجود داشته باشد، مقدار متناظر با آن بازگشت داده می‌شود و اگر خیر، delegate ایی که به عنوان پارامتر دوم آن معرفی شده‌است، اجرا خواهد شد، سپس مقدار بازگشت داده شده‌ی توسط آن به مجموعه اضافه شده و در آخر این مقدار به فراخوان بازگشت داده می‌شود.
var dictionary = new ConcurrentDictionary<string, string>();
 
var value = dictionary.GetOrAdd("key1", x => "item 1");
Console.WriteLine(value);
 
value = dictionary.GetOrAdd("key1", x => "item 2");
Console.WriteLine(value);
در این مثال زمانیکه اولین GetOrAdd فراخوانی می‌شود، مقدار item 1 بازگشت داده خواهد شد و همچنین این مقدار را در مجموعه‌ی جاری، به کلید key1 انتساب می‌دهد. در دومین فراخوانی، چون key1 در دیکشنری، دارای مقدار است، همان را بازگشت می‌دهد و دیگر به value factory ارائه شده مراجعه نخواهد کرد. بنابراین خروجی این مثال به صورت ذیل است:
item 1
item 1


دسترسی همزمان به متد GetOrAdd امن نیست

ConcurrentDictionary برای اغلب متدهای آن به صورت توکار مباحث قفل‌گذاری چند ریسمانی را اعمال می‌کند؛ اما نه برای متد GetOrAdd. زمانیکه valueFactory آن در حال اجرا است، دسترسی همزمان به آن thread-safe نیست و ممکن است بیش از یکبار فراخوانی شود.
یک مثال:
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;

namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            var dictionary = new ConcurrentDictionary<int, int>();
            var options = new ParallelOptions { MaxDegreeOfParallelism = 100 };
            var addStack = new ConcurrentStack<int>();

            Parallel.For(1, 1000, options, i =>
            {
                var key = i % 10;
                dictionary.GetOrAdd(key, k =>
                {
                    addStack.Push(k);
                    return i;
                });
            });

            Console.WriteLine($"dictionary.Count: {dictionary.Count}");
            Console.WriteLine($"addStack.Count: {addStack.Count}");
        }
    }
}
یک نمونه خروجی این مثال می‌تواند به صورت ذیل باشد:
dictionary.Count: 10
addStack.Count: 13
در اینجا هر چند 10 آیتم در دیکشنری ذخیره شده‌اند، اما عملیاتی که در value factory متد GetOrAdd آن صورت گرفته، 13 بار اجرا شده‌است (بجای 10 بار).
علت اینجا است که در این بین، متد GetOrAdd توسط ترد A فراخوانی می‌شود، اما key را در دیکشنری جاری پیدا نمی‌کند. به همین جهت شروع به اجرای valueFactory آن خواهد کرد. در همین زمان ترد B نیز به دنبال همین key است. ترد قبلی هنوز به پایان کار خودش نرسیده‌است که مجددا valueFactory متعلق به همین key اجرا خواهد شد. به همین جهت است که در ConcurrentStack اجرا شده‌ی در valueFactory، بیش از 10 آیتم موجود هستند.


الگویی برای مدیریت دسترسی همزمان امن به متد GetOrAdd‌

یک روش برای دسترسی همزمان امن به متد GetOrAdd، توسط تیم ASP.NET Core به صورت ذیل ارائه شده‌است:
// 'GetOrAdd' call on the dictionary is not thread safe and we might end up creating the pipeline more
// once. To prevent this Lazy<> is used. In the worst case multiple Lazy<> objects are created for multiple
// threads but only one of the objects succeeds in creating a pipeline.
private readonly ConcurrentDictionary<Type, Lazy<RequestDelegate>> _pipelinesCache = 
new ConcurrentDictionary<Type, Lazy<RequestDelegate>>();
در اینجا با استفاده از کلاس Lazy، از ایجاد چندین pipeline به ازای یک key مشخص جلوگیری شده‌است.
یک مثال:
namespace Sample
{
    class Program
    {
        static void Main(string[] args)
        {
            var dictionary = new ConcurrentDictionary<int, Lazy<int>>();
            var options = new ParallelOptions { MaxDegreeOfParallelism = 100 };
            var addStack = new ConcurrentStack<int>();

            Parallel.For(1, 1000, options, i =>
            {
                var key = i % 10;
                dictionary.GetOrAdd(key, k => new Lazy<int>(() =>
                {
                    addStack.Push(k);
                    return i;
                }));
            });

            // Access the dictionary values to create lazy values.
            foreach (var pair in dictionary)
                Console.WriteLine(pair.Value.Value);

            Console.WriteLine($"dictionary.Count: {dictionary.Count}");
            Console.WriteLine($"addStack.Count: {addStack.Count}");
        }
    }
}
با این خروجی:
10
1
2
3
4
5
6
7
8
9
dictionary.Count: 10
addStack.Count: 10
اینبار، هم dictionary و هم addStack دارای 10 عضو هستند که به معنای تنها اجرای 10 بار value factory است و نه بیشتر.
در این مثال دو تغییر صورت گرفته‌اند:
الف) مقادیر ConcurrentDictionary به صورت Lazy معرفی شده‌اند.
ب) متد GetOrAdd نیز یک مقدار Lazy را بازگشت می‌دهد.

زمانیکه از اشیاء Lazy استفاده می‌شود، خروجی‌های بازگشتی از GetOrAdd، توسط این اشیاء Lazy محصور خواهند شد. اما نکته‌ی مهم اینجا است که هنوز value factory آن‌ها فراخوانی نشده‌است. این فراخوانی تنها زمانی صورت می‌گیرد که به خاصیت Value یک شیء Lazy دسترسی پیدا کنیم و این دسترسی نیز به صورت thread-safe طراحی شده‌است. یعنی حتی اگر چند ترد new Lazy یک key مشخص را بازگشت دهند، تنها یکبار value factory متد GetOrAdd با دسترسی به خاصیت Value این اشیاء Lazy فراخوانی می‌شود و مابقی تردها منتظر مانده و تنها مقدار ذخیره شده‌ی در دیکشنری را دریافت می‌کنند و سبب اجرای مجدد value factory سنگین و زمانبر آن، نخواهند شد.

بر این مبنا می‌توان یک LazyConcurrentDictionary را نیز به صورت ذیل طراحی کرد:
    public class LazyConcurrentDictionary<TKey, TValue>
    {
        private readonly ConcurrentDictionary<TKey, Lazy<TValue>> _concurrentDictionary;
        public LazyConcurrentDictionary()
        {
            _concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>();
        }

        public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
        {
            var lazyResult = _concurrentDictionary.GetOrAdd(key,
             k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication));
            return lazyResult.Value;
        }
    }
در اینجا ممکن است چندین ترد همزمان متد GetOrAdd را دقیقا با یک کلید مشخص فراخوانی کنند؛ اما تنها چندین شیء Lazy بسیار سبک که هنوز اطلاعات محصور شده‌ی توسط آن‌ها اجرا نشده‌است، ایجاد خواهند شد. اولین تردی که به خاصیت Value آن دسترسی پیدا کند، سبب اجرای delegate زمانبر و سنگین آن شده و مابقی تردها مجبور به منتظر ماندن جهت بازگشت این نتیجه از دیکشنری خواهند شد (و نه اجرای مجدد delegate).
در مثال فوق، به صورت صریحی پارامتر LazyThreadSafetyMode نیز مقدار دهی شده‌است. هدف از آن اطمینان حاصل کردن از آغاز این شیء Lazy با دسترسی به خاصیت Value آن، تنها توسط یک ترد است.

نمونه‌ی دیگر کار با خاصیت ویژه‌ی Value شیء Lazy را در مطلب «پشتیبانی توکار از ایجاد کلاس‌های Singleton از دات نت 4 به بعد» پیشتر در این سایت مطالعه کرده‌اید.
مطالب دوره‌ها
نگاهی به SignalR Clients
در قسمت قبل موفق به ایجاد اولین Hub خود شدیم. در ادامه، برای تکمیل برنامه نیاز است تا کلاینتی را نیز برای آن تهیه کنیم.
مصرف کنندگان یک Hub می‌توانند انواع و اقسام برنامه‌های کلاینت مانند jQuery Clients و یا حتی یک برنامه کنسول ساده باشند و همچنین Hubهای دیگر نیز قابلیت استفاده از این امکانات Hubهای موجود را دارند. تیم SignalR امکان استفاده از Hubهای آن‌را در برنامه‌های دات نت 4 به بعد، برنامه‌های WinRT، ویندوز فون 8، سیلورلایت 5، jQuery و همچنین برنامه‌های CPP نیز مهیا کرده‌اند. به علاوه گروه‌های مختلف نیز با توجه به سورس باز بودن این مجموعه، کلاینت‌های iOS Native، iOS via Mono و Android via Mono را نیز به این لیست اضافه کرده‌اند.


بررسی کلاینت‌های jQuery

با توجه به پروتکل مبتنی بر JSON سیگنال‌آر، استفاده از آن در کتابخانه‌های جاوا اسکریپتی همانند jQuery نیز به سادگی مهیا است. برای نصب آن نیاز است در کنسول پاور شل نوگت، دستور زیر را صادر کنید:
 PM> Install-Package Microsoft.AspNet.SignalR.JS
برای نمونه به solution پروژه قبل، یک برنامه وب خالی دیگر را اضافه کرده و سپس دستور فوق را بر روی آن اجرا نمائید. در این حالت فقط باید دقت داشت که فرامین بر روی کدام پروژه اجرا می‌شوند:


با استفاده از افزونه SignalR jQuery، به دو طریق می‌توان به یک Hub اتصال برقرار کرد:
الف) استفاده از فایل proxy تولید شده آن (این فایل، در زمان اجرای برنامه تولید می‌شود و یا امکان استفاده از آن به کمک ابزارهای کمکی نیز وجود دارد)
نمونه‌ای از آن‌را در قسمت قبل ملاحظه کردید؛ همان فایل تولید شده در مسیر /signalr/hubs برنامه. به نوعی به آن Service contract نیز گفته می‌شود (ارائه متادیتا و قراردادهای کار با یک سرویس Hub). این فایل همانطور که عنوان شد به صورت پویا در زمان اجرای برنامه ایجاد می‌شود.
امکان تولید آن توسط برنامه کمکی signalr.exe نیز وجود دارد؛ برای دریافت آن می‌توان از طریق NuGet اقدام کرد (بسته Microsoft.AspNet.SignalR.Utils) که نهایتا در پوشه packages قرار خواهد گرفت. نحوه استفاده از آن نیز به صورت زیر است:
 Signalr.exe ghp http://localhost/
در این دستور ghp مخفف generate hub proxy است و نهایتا فایلی را به نام server.js تولید می‌کند.

ب) بدون استفاده از فایل proxy و به کمک روش late binding (انقیاد دیر هنگام)

برای کار با یک Hub از طریق jQuery مراحل ذیل باید طی شوند:
1) ارجاعی به Hub باید مشخص شود.
2) روال‌های رخدادگردان تنظیم گردند.
3) اتصال به Hub برقرار گردد.
4) متدی فراخوانی شود.

در اینجا باید دقت داشت که امکانات Hub به صورت خواص
 $.connection
در سمت کلاینت جی‌کوئری، در دسترس خواهند بود. برای مثال:
 $.connection.chatHub
و نام‌های بکارگرفته شده در اینجا مطابق روش‌های متداول نام گذاری در جاوا اسکریپت، camel case هستند.

خوب، تا اینجا فرض بر این است که یک پروژه خالی ASP.NET را آغاز و سپس فرمان نصب Microsoft.AspNet.SignalR.JS را نیز همانطور که عنوان شد، صادر کرده‌اید. در ادامه یک فایل ساده html را به نام chat.htm، به این پروژه جدید اضافه کنید (برای استفاده از کتابخانه جاوا اسکریپتی SignalR الزامی به استفاده از صفحات کامل پروژه‌های وب نیست).
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
    <script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {
            var chat;
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند
            chat = $.connection.chat; //این نام مستعار پیشتر توسط ویژگی نام هاب تنظیم شده است
            chat.client.hello = function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            };
            $.connection.hub.start(/*{ transport: 'longPolling' }*/); // فاز اولیه ارتباط را آغاز می‌کند

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                chat.server.sendMessage($("#txtMsg").val());
            });
        });
    </script>
</body>
</html>
کدهای آن‌را به نحو فوق تغییر دهید.
توضیحات:
همانطور که ملاحظه می‌کنید ابتدا ارجاعاتی به jquery و jquery.signalR-1.0.1.min.js اضافه شده‌اند. سپس نیاز است مسیر دقیق فایل پروکسی هاب خود را نیز مشخص کنیم. اینکار با تعریف مسیر signalr/hubs انجام شده است.
<script src="http://localhost:1072/signalr/hubs" type="text/javascript"></script>
در ادامه توسط تنظیم connection.hub.logging سبب خواهیم شد تا اطلاعات بیشتری در javascript console مرورگر لاگ شود.
سپس ارجاعی به هاب تعریف شده، تعریف گردیده است. اگر از قسمت قبل به خاطر داشته باشید، توسط ویژگی HubName، نام chat را برگزیدیم. بنابراین connection.chat ذکر شده دقیقا به این هاب اشاره می‌کند.
سپس سطر chat.client.hello مقدار دهی شده است. متد hello، متدی dynamic و تعریف شده در سمت هاب برنامه است. به این ترتیب می‌توان به پیام‌های رسیده از طرف سرور گوش فرا داد. در اینجا، این پیام‌ها، به li ایی با id مساوی messages اضافه می‌شوند.
سپس توسط فراخوانی متد connection.hub.start، فاز negotiation شروع می‌شود. در اینجا حتی می‌توان نوع transport را نیز صریحا انتخاب کرد که نمونه‌ای از آن را به صورت کامنت شده جهت آشنایی با نحوه تعریف آن مشاهده می‌کنید. مقادیر قابل استفاده در آن به شرح زیر هستند:
 - webSockets
- forverFrame
- serverSentEvents
- longPolling
سپس به رویدادهای کلیک دکمه send گوش فرا داده و در این حین، اطلاعات TextBox ایی با id مساوی txtMsg را به متد SendMessage هاب خود ارسال می‌کنیم. همانطور که پیشتر نیز عنوان شد، در سمت کلاینت، تعریف متد SendMessage باید camel case باشد.

اکنون به صورت جداگانه یکبار برنامه hub را در مرورگر باز کنید. سپس بر روی فایل chat.htm کلیک راست کرده و گزینه مشاهده آن را در مرورگر نیز انتخاب نمائید (گزینه View in browser منوی کلیک راست).

خوب! پروژه کار نمی‌کند! برای اینکه مشکلات را بهتر بتوانید مشاهده کنید نیاز است به JavaScript Console مرورگر خود مراجعه نمائید. برای مثال در مرورگر کروم دکمه F12 را فشرده و برگه Console آن‌را باز کنید. در اینجا اعلام می‌کند که فاز negotiation قابل انجام نیست؛ چون مسیر پیش فرضی را که انتخاب کرده است، همین مسیر پروژه دومی است که اضافه کرده‌ایم (کلاینت ما در پروژه دوم قرار دارد و نه در همان پروژه اول هاب).
برای اینکه مسیر دقیق hub را در این حالت مشخص کنیم، سطر زیر را به ابتدای کدهای جاوا اسکریپتی فوق اضافه نمائید:
 $.connection.hub.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
اکنون اگر مجدا سعی کنید، باز هم برنامه کار نمی‌کند و پیام می‌دهد که امکان دسترسی به این سرویس از خارج از دومین آن میسر نیست. برای اینکه این مجوز را صادر کنیم نیاز است تنظیمات مسیریابی پروژه هاب را به نحو ذیل ویرایش نمائیم:
using System;
using System.Web;
using System.Web.Routing;
using Microsoft.AspNet.SignalR;

namespace SignalR02
{
    public class Global : HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs(new HubConfiguration
            {
                EnableCrossDomain = true
            });
        }
    }
}
با تنظیم EnableCrossDomain به true اینبار فاز‌های آغاز ارتباط با سرور برقرار می‌شوند:
 SignalR: Auto detected cross domain url. jquery.signalR-1.0.1.min.js:10
SignalR: Negotiating with 'http://localhost:1072/signalr/negotiate'. jquery.signalR-1.0.1.min.js:10
SignalR: SignalR: Initializing long polling connection with server. jquery.signalR-1.0.1.min.js:10
SignalR: Attempting to connect to 'http://localhost:1072/signalr/connect?transport=longPolling&connectionToken…NRh72omzsPkKqhKw2&connectionData=%5B%7B%22name%22%3A%22chat%22%7D%5D&tid=3' using longPolling. jquery.signalR-1.0.1.min.js:10
SignalR: Longpolling connected jquery.signalR-1.0.1.min.js:10
مطابق این لاگ‌ها ابتدا فاز negotiation انجام می‌شود. سپس حالت long polling را به صورت خودکار انتخاب می‌کند.

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

یک نکته:
اگر از ویندوز 8 (یعنی IIS8) و VS 2012 استفاده می‌کنید، برای استفاده از حالت Web socket، ابتدا فایل وب کانفیگ برنامه را باز کرده و در قسمت httpRunTime، مقدار ویژگی targetFramework را بر روی 4.5 تنظیم کنید. اینبار اگر مراحل negotiation را بررسی کنید در همان مرحله اول برقراری اتصال، از روش Web socket استفاده گردیده است.


تمرین 1
به پروژه ساده و ابتدایی فوق یک تکست باکس دیگر به نام Room را اضافه کنید؛ به همراه دکمه join. سپس نکات قسمت قبل را در مورد الحاق به یک گروه و سپس ارسال پیام به اعضای گروه را پیاده سازی نمائید. (تمام نکات آن با مطلب فوق پوشش داده شده است و در اینجا باید صرفا فراخوانی متدهای عمومی دیگری در سمت هاب، صورت گیرد)

تمرین 2
در انتهای قسمت دوم به نحوه ارسال پیام از یک هاب به هابی دیگر اشاره شد. این MonitorHub را ایجاد کرده و همچنین یک کلاینت جاوا اسکریپتی را نیز برای آن تهیه کنید تا بتوان اتصال و قطع اتصال کلیه کاربران سیستم را مانیتور و مشاهده کرد.


پیاده سازی کلاینت jQuery بدون استفاده از کلاس Proxy

در مثال قبل، از پروکسی پویای مهیای در آدرس signalr/hubs استفاده کردیم. در اینجا قصد داریم، بدون استفاده از آن نیز کار برپایی کلاینت را بررسی کنیم.
بنابراین یک فایل جدید html را مثلا به نام chat_np.html به پروژه دوم برنامه اضافه کنید. سپس محتویات آن‌را به نحو زیر تغییر دهید:
<!DOCTYPE>
<html>
<head>
    <title></title>
    <script src="Scripts/jquery-1.6.4.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.signalR-1.0.1.min.js" type="text/javascript"></script>
</head>
<body>
    <div>
        <input id="txtMsg" type="text" /><input id="send" type="button" value="send msg" />
        <ul id="messages">
        </ul>
    </div>
    <script type="text/javascript">
        $(function () {                        
            $.connection.hub.logging = true; //اطلاعات بیشتری را در جاوا اسکریپت کنسول مرورگر لاگ می‌کند

            var connection = $.hubConnection();
            connection.url = 'http://localhost:1072/signalr'; //چون در یک پروژه دیگر قرار داریم
            var proxy = connection.createHubProxy('chat');

            proxy.on('hello', function (message) {
                //متدی که در اینجا تعریف شده دقیقا مطابق نام متد پویایی است که در هاب تعریف شده است
                //به این ترتیب سرور می‌تواند کلاینت را فراخوانی کند
                $("#messages").append("<li>" + message + "</li>");
            });

            $("#send").click(function () {
                // Hub's `SendMessage` should be camel case here
                proxy.invoke('sendMessage', $("#txtMsg").val());
            });

            connection.start();
        });
    </script>
</body>
</html>
در اینجا سطر مرتبط با تعریف مسیر اسکریپت‌های پویای signalr/hubs را دیگر در ابتدای فایل مشاهده نمی‌کنید. کار تشکیل proxy اینبار از طریق کدنویسی صورت گرفته است. پس از ایجاد پروکسی، برای گوش فرا دادن به متدهای فراخوانی شده از طرف سرور از متد proxy.on و نام متد فراخوانی شده سمت سرور استفاده می‌کنیم و یا برای ارسال اطلاعات به سرور از متد proxy.invoke به همراه نام متد سمت سرور استفاده خواهد شد.


کلاینت‌های دات نتی SignalR
تا کنون Solution ما حاوی یک پروژه Hub و یک پروژه وب کلاینت جی‌کوئری است. به همین Solution، یک پروژه کلاینت کنسول ویندوزی را نیز اضافه کنید.
سپس در خط فرمان پاور شل نوگت دستور زیر را صادر نمائید تا فایل‌های مورد نیاز به پروژه کنسول اضافه شوند:
 PM> Install-Package Microsoft.AspNet.SignalR.Client
در اینجا نیز باید دقت داشت تا دستور بر روی default project صحیحی اجرا شود (حالت پیش فرض، اولین پروژه موجود در solution است).
پس از نصب آن اگر به پوشه packages مراجعه کنید، نگارش‌های مختلف آن‌را مخصوص سیلورلایت، دات نت‌های 4 و 4.5، WinRT و ویندوز فون8 نیز می‌توانید در پوشه Microsoft.AspNet.SignalR.Client ملاحظه نمائید. البته در ابتدای نصب، انتخاب نگارش مناسب، بر اساس نوع پروژه جاری به صورت خودکار صورت می‌گیرد.
مدل برنامه نویسی آن نیز بسیار شبیه است به حالت عدم استفاده از پروکسی در حین استفاده از jQuery که در قسمت قبل بررسی گردید و شامل این مراحل است:
1) یک وهله از شیء HubConnection را ایجاد کنید.
2) پروکسی مورد نیاز را جهت اتصال به Hub از طریق متد CreateProxy تهیه کنید.
3) رویدادگردان‌ها را همانند نمونه کدهای جاوا اسکریپتی قسمت قبل، توسط متد On تعریف کنید.
4) به کمک متد Start، اتصال را آغاز نمائید.
5) متدها را به کمک متد Invoke فراخوانی نمائید.

using System;
using Microsoft.AspNet.SignalR.Client.Hubs;

namespace SignalR02.WinClient
{
    class Program
    {
        static void Main(string[] args)
        {
            var hubConnection = new HubConnection(url: "http://localhost:1072/signalr");
            var chat = hubConnection.CreateHubProxy(hubName: "chat");

            chat.On<string>("hello", msg => {
                Console.WriteLine(msg);
            });

            hubConnection.Start().Wait();

            chat.Invoke<string>("sendMessage", "Hello!");

            Console.WriteLine("Press a key to terminate the client...");
            Console.Read();
        }
    }
}
نمونه‌ای از این پیاده سازی را در کدهای فوق ملاحظه می‌کنید که از لحاظ طراحی آنچنان تفاوتی با نمونه ذهنی جاوا اسکریپتی ندارد.

نکته مهم
کلیه فراخوانی‌هایی که در اینجا ملاحظه می‌کنید غیرهمزمان هستند.
به همین جهت پس از متد Start، متد Wait ذکر شده‌است تا در این برنامه ساده، پس از برقراری کامل اتصال، کار invoke صورت گیرد و یا زمانیکه callback تعریف شده توسط متد chat.On فراخوانی می‌شود نیز این فراخوانی غیرهمزمان است و خصوصا اگر نیاز است رابط کاربری برنامه را در این بین به روز کنید باید به نکات به روز رسانی رابط کاربری از طریق یک ترد دیگر دقت داشت.
مطالب
ایجاد چارت سازمانی تحت وب #4 - آخر
نما : Layout
در یک نمودار یا چارت سازمانی در حد امکان شاخه‌ها همواره در کنار هم و جمع و جور رسم میشوند. در مثال زیر نود u-Node 1 و u-Node 3 دارای زیر شاخه نبوده ، بنابراین نیازی به فضای زیرین جهت نمایش ندارند. جهت مشاهده این فضا میتوانید خط مشخص شده در کد را فعال نمائید و تفاوت فضای مورد نیاز و ایجاد شده را ببینید.
دو درخت نمودار متفاوت در کنار هم رسم شده اند. هیچ همپوشانی بین درختان رسم شده وجود ندارد ( بنابراین نود Root 2 روی نود u-Node 3  رسم نشده است.


var o = new orgChart();

o.addNode( 0, '', '', 'Root 1');
o.addNode( 1,  0, 'u', 'u-Node 1');
o.addNode( 2,  0, 'u', 'u-Node 2');
o.addNode( 3,  0, 'u', 'u-Node 3');
o.addNode( 4,  0, 'u', 'u-Node 4');
//میتوانید خط زیر را فعال نمائید تا تفاوت فضای ایجاد شده و مورد نیاز را مشاهده نمائید.
 //o.addNode( 9, 3, 'u', 'EXTRA', 0, '', '#CC0000', '#FF0000'); o.addNode(11, 2, 'l', 'l-Node'); o.addNode(12, 2, 'u', 'u-Node'); o.addNode(13, 2, 'u', 'u-Node'); o.addNode(14, 2, 'r', 'r-Node'); o.addNode(20, 4, 'l', 'l-Node'); o.addNode(21, '', '', 'Root 2'); o.drawChart('c_layout');
یک مثال کامل :
نمونه زیر یک مثال کامل میباشد. انواع اتصالهای تو در تو چندگانه در این نمونه استفاده شده است .


این هم کد نمونه فوق :

var o = new orgChart();

o.setFont('Arial', 18);
o.addNode( 0, '', '', 'President', 1);

o.setFont('Arial', 12);
o.addNode('',  0, 'l', 'Control');
o.addNode('',  0, 'l', 'Secretary');
o.addNode('',  0, 'l', 'Marketing');
o.addNode('',  0, 'l', 'Human resources');

o.setColor('#99CC99', '#CCFFCC');
o.addNode(12,  0, 'r', 'Facility');
o.addNode('', 12, 'r', 'IT');
o.addNode('', 12, 'r', 'Resource planning');

o.setFont('Arial', 18);
o.setColor('#CCCC66', '#FFFF99');
o.addNode(20,  0, 'u', 'Projects', 1);

o.setFont('Arial', 12);
o.addNode('', 20, 'r', 'Management');
o.addNode('', 20, 'r', 'Finance');
o.addNode('', 20, 'l', 'Development');
o.addNode('', 20, 'l', 'Maintenance');
o.addNode('', 20, 'l', 'Specials');

o.setColor('#CC4950', '#FF7C80');
o.setFont('Arial', 18);
o.addNode(30,  0, 'u', 'Specials', 1);

o.setFont('Arial', 12);
o.addNode(31, 30, 'l', 'Management');
o.addNode('', 30, 'l', 'Communication');
o.addNode(33, 30, 'r', 'Development');
o.addNode(34, 33, 'r', 'Maintenance');
o.addNode('', 33, 'r', 'Special A');
o.addNode('', 33, 'r', 'Special B');
o.addNode('', 33, 'r', 'Advice');
o.addNode('', 30, 'l', 'Taskforce');

o.setColor('#CC9966', '#FFCC99');
o.setFont('Arial', 18);
o.addNode(40,  0, 'u', 'Programming', 1);

o.setFont('Arial', 12);
o.addNode(41, 40, 'l', 'Management');
o.addNode(42, 40, 'l', 'Finance');
o.addNode('', 40, 'r', 'Assessment');
o.addNode('', 40, 'r', 'Coding team');
o.addNode('', 40, 'r', 'Quality control');

o.drawChart('c_ex1', '', true);
اضافه کردن تصویر به نودها :
شما میتوانید به نودها تصویر دلخواه خود را نیز اضافه نمائید. تصاویر بصورت عمودی قرار خواهند گرفت و در صورتی که بزرگ باشند تغییر اندازه خواهند داد. ( فراخوانی تابع setSize قبل از اضافه کردن عکس در این مثال )

کدهای مثال فوق :

var o = new orgChart();

o.setSize(120, 60);

o.setFont('Arial', 18);
o.addNode( 1, '', '', 'Icon smiley', 0, '', '', '', '', 'pic/smiley.gif');
o.addNode( 2, '', '', 'This is a tree', 0, '', '', '', '', 'pic/tree.jpg');
o.addNode( 3, 2, 'u', 'This is a tree');
o.addNode( 4, '', '', 'Right Top smiley', 0, '', '', '', '', 'pic/smiley.gif', 'rt');
o.addNode( 5, '', '', 'Center bottom smiley', 0, '', '', '', '', 'pic/smiley.gif', 'cb');

o.drawChart('c_img');

یک مثال دیگر از استفاده تصاویر در چارت :


var o = new orgChart();

o.setSize(60, 110);

o.setFont('Arial', 12);
o.addNode( 1, '', '', 'Hominidae');
o.addNode( 10, 1, 'l', 'Hominidae');
o.addNode( 11, 10, 'l', 'Hominini');
o.addNode( 12, 10, 'r', 'Gorillini');
o.addNode( 20, 1, 'r', 'Ponginae');
o.addNode( '', 11, '', 'Homo Sapiens', '', '', '', '', '', 'pic/homo.jpg', 'ct');
o.addNode( '', 11, '', 'Pan', '', '', '', '', '', 'pic/pan.jpg', 'ct');
o.addNode( '', 12, '', 'Gorilla', '', '', '', '', '', 'pic/gorilla.jpg', 'ct');
o.addNode( '', 20, '', 'Pongo', '', '', '', '', '', 'pic/pongo.jpg', 'ct');

o.drawChart('c_img2', 'c');
تبدیل تصویر از فرمت jpg به تصویر با فرمت png :
عدم امکان استفاده از مرورگر IE تا نسخه 8 ، چرا که IE  هیچ پشتیبانی از toDataURL در excanvas.js  را انجام نمیدهد.
شما میتوانید از توابع استاندارد canvas در جهت تبدیل محتویات canvas به تصویر استاتیک استفاده نمائید. برای اینکه بتوانید این کار را تست نمائید باید کد ذیل را در همان صفحه ای که کد ( یک مثال کامل ) را استفاده کردید درج نمائید و در این صورت با کلیک بر روی لینک اول میتوانید یک تصویر با فرمت png در یک صفحه جدید از نمودار خود بدست بیاورید و یا با کلیک بر روی لینک دوم یک تصویر را دانلود نمائید.
<script type="text/javascript">
function openAsPng(id){
        window.open(document.getElementById(id).toDataURL("image/png"));
}
function saveAsPng(id){
var img = document.getElementById(id).toDataURL("image/png");
        document.location.href = img.replace("image/png", "image/octet-stream");
}
</script>

<a href = "javascript:openAsPng('c_ex1');">Click here to open the image as png in a new window</a><BR>
<a href = "javascript:saveAsPng('c_ex1');">Click here to save the image as png</a><BR>

نمایش چارت فقط بصورت یک تصویر :

برای اینکه بتوانید یک چارت ایجاد شده از این روش را فقط بصورت یک تصویر نمایش دهید باید عمل تبدیل به عکس را بلافاصله پس از رسم نمودار در canvas انجام دهید بدین صورت که در کد ذیل مشاهده مینمائید:
<canvas id="c_pngchart" width="1" height="1">Your browser does not support canvas!</canvas>

<img id="pngchart">
<script type="text/javascript">
var o = new orgChart();
o.addNode(0, '', '', 'Root');
o.addNode(1, 0, 'u', 'u-Node 1');
o.addNode(2, 0, 'u', 'u-Node 2');
o.drawChart('c_pngchart', '', true);
var canvas = document.getElementById('c_pngchart');
document.getElementById("pngchart").src = canvas.toDataURL("image/png");
// The html keyword "hidden" doesn't work in IE, so resize the canvas to NUL
canvas.height = 0;
canvas.width = 0;
</script>

همه لینکهایی که در نودها ایجاد شده است غیرفعال شده و از کار می‌افتند. بنابراین برای انجام این کار ،یک المنت تصویر در صفحه خود ایجاد ، نمودار را در canvas رسم نموده ، نمودار را به تصویر تبدیل نموده و آن را به المنت تصویر مقید میکنیم و در آخر canvas مخفی میکنیم. برای این منظور از کلید واژه hidden  استفاده میکنیم که در IE این کلمه باز قابل شناسایی نبوده و باید از روش تخصیص اندازه طول و عرض صفر 0 استفاده شود یعنی width=0 , height=0

تصویر فوق ایجاد شده کد مورد نظر میباشد.

تغییر اندازه پویا :
اگر بخواهید بصورت پویا اندازه canvas را تغییر دهید ، نمودار شما ناپدید میشود و پس از تغییر اندازه ، نمودار پاک خواهد شد.
برای رسم نمودار باید دوباره از توابع drawChart() یا redrawChart()   استفاده نمائید.
برای رسم نودها نیازی به تعریف دوباره آنها نمیباشد ( مخصوصا در مثالی که در این صفحه برای شما ارائه شده است )
تابع ()drowChart تمامی نودها را در زمان رسم دوباره جاگذاری میکند ، در صورتی که اگر شما میدانید چارت شما به غیر از اندازه هیچ تغییر دیگری نداشته میتوانید با فراخوانی تابع redrawChart  یک کپی از همان چارت را که در حافظه canvas وجود دارد را رسم نمائید.
از تمامی دوستان خوبم تشکر میکنم که این مطلب را دنبال نمودند . ما را از نظرات خوب و سازنده خود بی نصیب نفرمائید.

نظرات مطالب
ارتقاء به ASP.NET Core 2.0 - معرفی بسته‌ی Microsoft.AspNetCore.All
ارتقاء به ASP.NET Core 3.0 و سرنوشت metapackageهای Microsoft.AspNetCore

پس از نصب SDK جدید، اگر دستور dotnet new mvc را صادر کنید، فایل csproj تولید شده‌ی آن تنها دارای TargetFramework ای معادل netcoreapp3.0 است و نه هیچ مورد دیگری:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
  </ItemGroup>
</Project>
بنابراین برای ارتقاء پروژه‌های قبلی به نگارش 3 آن:
- ابتدا TargetFramework را به netcoreapp3.0 تنظیم کنید.
- سپس تمام PackageReferenceهایی را که به بسته‌های Microsoft.AspNetCore.All و یا Microsoft.AspNetCore.App اشاره می‌کنند، حذف کنید.
- ارجاع به بسته‌ی Microsoft.AspNetCore.Razor.Design را نیز حذف کنید.
- اگر پیشتر خاصیت AspNetCoreHostingModel را به حالت درون پروسه‌ای تنظیم کرده‌اید، آن‌را حذف کنید؛ چون حالت پیش‌فرض نگارش 3 است.
- حذف شدن JSON.NET را مدنظر داشته باشید.
- تغییرات حالت ثبت سرویس‌های MVC و Razor Pages و Web API را مدنظر داشته باشید.
- مسیریابی آن نیز Endpoint routing شده‌است.
- نقطه‌ی آغازین آن نیز بازنویسی شده‌است.
نظرات مطالب
تنظیمات CORS در ASP.NET Core
با توجه به پیاده‌سازی  CORS-RFC1918  در مرورگر کروم، با وجود پذیرش درخواست‌های CORS، از درخواست‌هایی که از public-network به منابعی از private-network ارسال شوند جلوگیری می‌شود.
به طور مثال از درخواست‌هایی که از طرف یک سایت اینترنتی به سرویس‌های در حال اجرا بر روی کلاینت (مانند سرویس‌های وبی دستگاه‌های پوز) ارسال می‌شود، جلوگیری می‌شود.
با غیر فعال کردن chrome://flags/#block-insecure-private-network-requests  این رفتار کروم را می‌توان تغییر داد.
مطالب
SQL Antipattern #2

بخش دوم : Naive Trees  

فرض کنید یک وب سایت حرفه‌ای خبری یا علمی-پژوهشی داریم که قابلیت دریافت نظرات کاربران را در مورد هر مطلب مندرج در سایت یا نظرات داده شده در مورد آن مطالب را دارا می‌باشد. یعنی هر کاربر علاوه بر توانایی اظهار نظر در مورد یک خبر یا مطلب باید بتواند پاسخ نظرات کاربران دیگر را نیز بدهد. اولین راه حلی که برای طراحی این مطلب در پایگاه داده به ذهن ما می‌رسد، ایجاد یک زنجیره با استفاده از کد sql زیر می‌باشد:

CREATE TABLE Comments (
comment_idSERIAL PRIMARY KEY,
parent_idBIGINT UNSIGNED,
comment TEXT NOT NULL,
FOREIGN KEY (parent_id) REFERENCES Comments(comment_id)
);

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

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

1.2 هدف: ذخیره و ایجاد پرس‌وجو در سلسله‌مراتب

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

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

Organizational chart: در این ساختار برای مثال در ارتباط بین کارمندان و مدیر، هر کارمند یک مدیر دارد که نشان‌دهنده‌ی پدر یک کارمند در ساختار درختی است. هر مدیر هم یک کارمند محسوب می‌گردد.

Threaded discussion: در این ساختار برای مثال در سیستم نظردهی و پاسخ‌ها، ممکن است زنجیره‌‌ای از نظرات در پاسخ به نظرات دیگر استفاده گردد. در درخت، فرزندان یک گره‌ی نظر، پاسخ‌های آن گره هستند.

در این فصل ما از مثال ساختار دوم برای نشان دادن Antipattern و راه حل آن بهره می‌گیریم.

2.2 Antipattern : همیشه مبتنی بر یکی از والدین

راه حل ابتدایی و ناکارآمد  

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

 CREATE TABLE Comments (  comment_idSERIAL PRIMARY KEY,
parent_idBIGINT UNSIGNED,
bug_idBIGINT UNSIGNED NOT NULL,
author BIGINT UNSIGNED NOT NULL,
comment_dateDATETIME NOT NULL,
comment TEXT NOT NULL,
FOREIGN KEY (parent_id)REFERENCES Comments(comment_id),
FOREIGN KEY (bug_id)         REFERENCES Bugs(bug_id),
FOREIGN KEY(author)          REFERENCES Accounts(account_id)
);

مثالی از پرس‌وجوی فوق را می‌توانید در زیر ببینید: 

لیست مجاورت :

لیست مجاورت خود می‌تواند به عنوان یک antipattern در نظر گرفته شود. البته این مطلب از آنجایی نشأت می‌گیرد که این روش توسط بسیاری از توسعه‌دهندگان مورد استفاده قرار می‌گیرد ولی نتوانسته است به عنوان راه حل برای معمول‌ترین وظیفه‌ی خود، یعنی ایجاد پرس‌وجو بر روی کلیه فرزندان، باشد.

• با استفاده از پرس‌وجوی زیر می‌توان فرزند بلافاصله‌ی یک "نظر" را برگرداند: 

SELECT c1.*, c2.*
FROM Comments c1 LEFT OUTER JOIN Comments c2
ON c2.parent_id = c1.comment_id;

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

SELECT c1.*, c2.*, c3.*, c4.*
FROM Comments c1                         -- 1st level
LEFT OUTER JOIN Comments c2
ON c2.parent_id = c1.comment_id  -- 2nd level
LEFT OUTER JOIN Comments c3
ON c3.parent_id = c2.comment_id  -- 3rd level
LEFT OUTER JOIN Comments c4
ON c4.parent_id = c3.comment_id; -- 4th level

این پرس‌وجو به این دلیل که با اضافه شدن ستون‌های دیگر، نوه‌ها را سطوح عمیق‌تری برمی‌گرداند، پرس‌وجوی مناسبی نیست. در واقع استفاده از توابع تجمیعی ، مانند COUNT() مشکل می‌شود.

راه دیگر برای به دست آوردن ساختار یک زیردرخت از لیست مجاورت برای یک برنامه، این است که سطرهای مورد نظر خود را از مجموعه بازیابی نموده و سلسه‌مراتب مورد نظر را در حافظه بازیابی نماییم و از آن به عنوان درخت استفاده نماییم:

   SELECT * FROM Comments WHERE bug_id = 1234;


نگهداری کردن یک درخت با استفاده از لیست مجاورت
البته برخی از عملکردها با لیست مجاورت به خوبی انجام می‌گیرد. برای مثال اضافه نمودن یک گره  (نظر)، مکان‌یابی مجدد برای یک گره یا یک زیردرخت .
INSERT INTO Comments (bug_id, parent_id, author, comment)
VALUES (1234, 7, 'Kukla' , 'Thanks!' );

بازیابی دوباره مکان یک نود یا یک زیردرخت نیز آسان است: 
UPDATE Comments SET parent_id = 3 WHERE comment_id = 6;

با این حال حذف یک گره از یک درخت در این روش پیچیده است. اگر بخواهیم یک زیردرخت را حذف کنید باید چندین پرس‌وجو برای پیدا کردن تمام نوه‌ها بنویسیم و سپس حذف نوه‌ها را از پایین‌ترین سطح شروع کرده و تا جایی که قید کلید خارجی برقرار شود ادامه دهیم. البته می‌توان از کلید خارجی با تنظیم ON DELETE CASCADE  استفاده کرد تا این کارها به طور خودکار انجام گیرد.
حال اگر بخواهیم یک نود غیر برگ را حذف کرده یا فرزندان آن را در درخت جابجا کنیم، ابتدا باید parent_id فرزندان آن نود را تغییر داده و سپس نود مورد نظر را حذف می‌کنیم:
SELECT parent_id FROM Comments WHERE comment_id = 6; -- returns 4
UPDATE Comments SET parent_id = 4 WHERE parent_id = 6;
DELETE FROM Comments WHERE comment_id = 6;


3.2 موارد تشخیص این Antipattern:
سؤالات زیر نشان می‌دهند که Naive Trees antipattern مورد استفاده قرار گرفته است:
  • چه تعداد سطح برای پشتیبانی در درخت نیاز خواهیم داشت؟
  • من همیشه از کار با کدی که ساختار داده‌ی درختی را مدیریت می‌کند، می‌ترسم
  • من باید اسکریپتی را به طور دوره‌ای اجرا نمایم تا سطرهای یتیم موجود در درخت را حذف کند.

4.2 مواردی که استفاده از این Antipattern مجاز است:
قدرت لیست مجاورت در بازیابی پدر یا فرزند مستقیم یک نود می‌باشد. قرار دادن یک سطر هم در لیست مجاورت کار ساده‌ای است. اگر این عملیات، تمام آن چیزی است که برای انجام کارتان مورد نیاز شما است، بنابراین استفاده از لیست مجاورت می‌تواند مناسب باشد.
برخی از برندهای RDBMS از افزونه‌هایی پشتیبانی می‌کنند که قابلیت ذخیره‌ی سلسله مراتب را در لیست مجاورت ممکن می‌سازد. مثلا SQL-99، پرس‌وجوی بازگشتی را تعریف می‌کند که مثال آن در ادامه آمده است:
  WITH CommentTree (comment_id, bug_id, parent_id, author, comment, depth)
AS (
SELECT *, 0 AS depth FROM Comments
WHERE parent_id IS NULL
UNION ALL
SELECT c.*, ct.depth+1 AS depth FROM CommentTreect
JOIN Comments c ON (ct.comment_id = c.parent_id)
)
SELECT * FROM CommentTree WHERE bug_id = 1234;

Microsoft SQL Server 2005، Oracle 11g، IBM DB2 و PostgreSQL 8.4 نیز از پرس‌وجوی بازگشتی پشتیبانی می‌کنند.Oracle 9i و 10g از عبارت WITH استفاده می‌کنند، ولی نه برای پرس‌وجوهای بازگشتی. در عوض می‌توانید از پرس‌وجوی زیر برای ایجاد پرس‌وجوی بازگشتی استفاده نمایید: 
SELECT * FROM Comments
START WITH comment_id = 9876
CONNECT BY PRIOR parent_id = comment_id;


5.2 راه حل: استفاده از مدل‌های درختی دیگر
جایگزین‌های دیگری برای ذخیره‌سازی داده‌های سلسله مراتبی وجود دارد. البته برخی از این راه حل‌ها ممکن است در لحظه‌ی اول پیچید‌تر از لیست مجاورت به نظر آیند، ولی برخی از عملیات درخت که در لیست مجاورت بسیار سخت یا ناکارآمد است، را آسان‌تر می‌کنند.
شمارش مسیر :
مشکل پرهزینه بودن بازیابی نیاکان یک گره که در روش لیست مجاورت وجود داشت در روش شمارش مسیر به این ترتیب حل شده است: اضافه نمودن یک صفت به هر گره که رشته‌ای از نیکان آن صفت در آن ذخیره شده است.
در جدول Comments به جای استفاده از parent_id، یک ستون به نام path که توع آن varchar است تعریف شده است. رشته‌ای که در این ستون تعریف شده است، ترتیبی از فرزندان این سطر از بالا به پایین درخت است. مانند مسیری که در سیستم عامل UNIX، برای نشان دادن مسیر در سیستم فایل استفاده شده است. شما می‌توانید از / به عنوان کاراکتر جداکننده استفاده نمایید. دقت کنید برای درست کار کردن پرس‌وجوها حتما در آخر مسیر هم این کاراکتر را قرار دهید. پرس‌وجوی تشکیل چنین درختی به شکل زیر است:
  CREATE TABLE Comments ( comment_id SERIAL PRIMARY KEY,
path VARCHAR(1000),
bug_id BIGINT UNSIGNED NOT NULL,
author BIGINT UNSIGNED NOT NULL,
comment_date DATETIME NOT NULL,
comment TEXT NOT NULL,
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id),
FOREIGN KEY (author) REFERENCES Accounts(account_id)

در این روش، هر گره مسیری دارد که شماره خود آن گره هم در آنتهای آن مسیر قرار دارد. این به دلیل درست جواب دادن پرس‌وجوهای ایجاد شده است.
می‌توان نیاکان را با مقایسه‌ی مسیر سطر کنونی با مسیر سطر دیگر به دست آورد. برای مثال برای یافتن نیاکان گره (نظر) شماره‌ی 7 که مسیر آن 1/4/6/7/ می‌باشد، می‌توان چنین نوشت:
  SELECT * FROM Comments AS c
WHERE '1/4/6/7/' LIKE c.path || '%' ;

این پرس‌وجو الگوهایی را می‌یابد که از مسیرهای 1/4/6/%، 1/4/% و 1/% نشأت می‌گیرد.
همچنین فرزندان (نوه‌های) یک گره، مثلا گره‌ی 4 را که مسیرش 1/4/ است را می‌توان با پرس‌وجوی زیر یافت:
  SELECT * FROM Comments AS c
WHERE c.path LIKE '1/4/' || '%' ;

الگوی 1/4/% با مسیرهای 1/4/5/، 1/4/6/ و 1/4/6/7/ تطابق می‌یابد.
همچنین می‌توان پرس‌وجوهای دیگری را نیز در این مسیر به سادگی انجام داد؛ مانند محاسبه‌ی مجموع هزینه‌ی گره‌ها در یک زیردرخت یا شمارش تعداد گره‌ها.
اضافه نمودن یک گره هم مانند ساختن خود مدل است. می‌توان یک گره‌ی غیر برگ را بدون نیاز به اصلاح هیچ سطری اضافه نمود. برای این کار مسیر را را از گره‌ی پدر کپی کرده و در انتها شماره‌ی خود گره را به آن اضافه می‌کنیم.
از مشکلات این روش می‌توان به عدم توانایی پایگاه داده‌ها در تحمیل این نکته که مسیر یک گره درست ایجاد شده است و یا تضمین وجود گره‌ای در مسیری خاص، اشاره نمود. همچنین نگهداری رشته‌ی مسیر یک گره مبتنی بر کد برنامه است و اعتبارسنجی آن کاری هزینه‌بر است. این رشته اندازه‌ای محدود دارد و درخت‌هایی با عمق نامحدود را پشتیبانی نمی‌کند.

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

 این اطلاعات می‌توانند به وسیله‌ی هر گره‌ای که در درخت با دو شماره‌ی nsleft و nsright ذخیره شده، نمایش داده شوند:
  CREATE TABLE Comments ( comment_id SERIAL PRIMARY KEY,
nsleft INTEGER NOT NULL,
nsright INTEGER NOT NULL,
bug_id BIGINT UNSIGNED NOT NULL,
author BIGINT UNSIGNED NOT NULL,
comment_date DATETIME NOT NULL,
comment TEXT NOT NULL,
FOREIGN KEY (bug_id) REFERENCES Bugs (bug_id),
FOREIGN KEY (author) REFERENCES Accounts(account_id)
);

شماره‌ی سمت چپ یک گره از تمام شماره‌های سمت چپ فرزندان آن گره کوچک‌تر و شماره‌ی سمت راست آن گره از تمام شماره‌های سمت راست آن گره بزرگ‌تر است. این شماره‌ها هیچ ارتباطی به comment_id مربوط به آن گره ندارند.

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

با اختصتص شماره‌ها به هر گره، می‌توان از آن‌ها برای یافتن نیاکان و فرزندان آن گره بهره جست. برای مثال برای بازیابی گره‌ی 4 و فرزندان (نوه‌های) آن باید دنبال گره‌هایی باشیم که شماره‌های آن گره‌ها بین nsleft و nsright گره‌ی شماره‌4 باشد:

  SELECT c2.* FROM Comments AS c1
JOIN Comments as c2
ON c2.nsleft BETWEEN c1.nsleft AND c1.nsright
WHERE c1.comment_id = 4;

همچنین می‌توان گره‌ی شماره‌ی 6 و نیاکان آن را با دنبال نمودن گره‌هایی به دست آورد که شماره‌های آن‌ها در محدوده‌ی شماره‌ی گره‌ی 6 باشد: 
SELECT c2.*
FROM Comments AS c1
JOIN Comment AS c2
ON c1.nsleft BETWEEN c2.nsleft AND c2.nsright
WHERE c1.comment_id = 6;

یک مزیت مهم روش مجموعه‌ای تودرتو، این است که هنگامی که یک گره را حذف می‌کنیم، نوه‌های آن به طور مستقیم به عنوان فرزندان پدر گره‌ی حذف شده تلقی می‌شوند.
برخی از پرس‌وجوهایی که در روش لیست مجاورت ساده بودند، مانند بازیابی فرزند یا پدر بلافصل، در روش مجموعه‌های تودرتو پیچیده‌تر می‌باشند. برای مثال برای یافتن پدر بلافصل گره‌ی شماره‌ی 6 باید چنین نوشت: 
  SELECT parent.* FROM Comment AS c
JOIN Comment AS parent
ON c.nsleft BETWEEN parent.nsleft AND parent.nsright
LEFT OUTER JOIN Comment AS in_between
ON c.nsleft BETWEEN in_between.nsleft AND in_between.nsright
AND in_between.nsleft BETWEEN parent.nsleft AND parent.nsright
WHERE c.comment_id = 6
AND in_between.comment_id IS NULL;

دست‌کاری درخت، اضافه، حذف و جابجا نمودن گره‌ها در آن درروش مجموعه‌های تودرتو از مدل‌های دیگر پیچیده‌تر است. هنگامی که یک گره‌ی جدید را اضافه می‌کنیم، باید تمام مقادیر چپ و راست بزرگ‌تر از مقدار سمت چپ گره‌ی جدید را مجددا محاسبه کنیم؛ که این شامل برادر سمت راست گره‌ی جدید، نیاکان آن و برادر سمت راست نیاکان آن می‌باشد. همچنین اگر گره‌ی جدید به عنوان گره‌ی غیربرگ اضافه شده باشد، شامل فرزندان آن هم می‌شود. برای مثال اگر بخواهیم گره‌ی جدیدی به گره‌ی 5 اضافه نماییم، باید چنین بنویسیم: 
-- make space for NS values 8 and 9
UPDATE Comment
SET nsleft = CASE WHEN nsleft >= 8 THEN nsleft+2 ELSE nsleft END,
nsright = nsright+2
WHERE nsright >= 7;

-- create new child of comment #5, occupying NS values 8 and 9
INSERT INTO Comment (nsleft, nsright, author, comment)
VALUES (8, 9, 'Fran' , 'Me too!' );

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

Closure Table
راه حل closure table روشی دیگر برای ذخیره‌ی سلسه‌مراتبی است. این روش علاوه بر ارتباطات مستقیم پدر- فرزندی، تمام مسیرهای موجود در درخت را ذخیره می‌کند.

این روش علاوه بر داشتن یک جدول نظرها، یک جدول دیگر به نام TreePaths با دو ستون دارد که هر کدام از این ستون‌ها یک کلید خارجی به جدولComment هستند:
  CREATE TABLE Comments ( comment_id SERIAL PRIMARY KEY,
bug_id BIGINT UNSIGNED NOT NULL,
author BIGINT UNSIGNED NOT NULL,
comment_date DATETIME NOT NULL,
comment TEXT NOT NULL,
FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id),
FOREIGN KEY (author) REFERENCES Accounts(account_id)
);
CREATE TABLE TreePaths (
ancestor BIGINT UNSIGNED NOT NULL,
descendant BIGINT UNSIGNED NOT NULL,
PRIMARY KEY(ancestor, descendant),
FOREIGN KEY (ancestor) REFERENCES Comments(comment_id),
FOREIGN KEY (descendant) REFERENCES Comments(comment_id)
);

به جای استفاده از جدول Comments برای ذخیره‌ی اطلاعات مربوط به یک درخت از جدول TreePath استفاده می‌کنیم. به ازای هر یک جفت گره در این درخت یک سطر در جدول ذخیره می‌شود که ارتباط پدر فرزندی را نمایش می‌دهد و الزاما نباید این دو پدر فرزند بلافصل باشد. همچنین یک سطر هم به ازای ارتباط هر گره با خودش به جدول اضافه می‌گردد.

پرس‌وجوهای بازیابی نیاکان و فرزندان (گره‌ها) از طریق جدول TreePaths ساده‌تر از روش مجموعه‌های تودرتو است. مثلا برای بازیابی فرزندان (نوه‌های) گره‌ی شماره‌ی 4، سطرهایی که نیاکان آن‌ها 4 است را به دست می‌آوریم:

   SELECT c.*  FROM Comments AS c
JOIN TreePaths AS t ON c.comment_id = t.descendant
WHERE t.ancestor = 4;

برای به دست آوردن نیاکان گره‌ی شماره‌ی 6، سطرهایی از جول TreePaths را به دست می‌آوریم که فرزندان آن‌ها 6 باشد:
SELECT c.*
FROM Comments AS c
JOIN TreePaths AS t ON c.comment_id = t.ancestor
WHERE t.descendant = 6;

برای اضافه کردن گره‌ی جدید، برای مثال به عنوان فرزند گره‌ی شماره‌ی 5، ابتدا سطری که به خود آن گره برمی‌گردد را اضافه می‌کنیم، سپس یک کپی از سطوری که در جدول TreePaths، به عنوان فرزندان (نوه‌های) گره‌ی شماره‌5 هستند (که شامل سطری که به خود گره‌ی 5 به عنوان فرزند اشاره می‌کند) به جدول اضافه نموده و فیلد descendant آن را با شماره‌ی گره‌ی جدید جایگزین می‌کنیم:
  INSERT INTO TreePaths (ancestor, descendant) SELECT t.ancestor, 8
FROM TreePaths AS t
WHERE t.descendant = 5
UNION ALL
SELECT 8, 8;

در این جا می‌توان به اهمیت ارجاع یک گره به خودش به عنوان پدر (یا فرزند) پی برد.
برای حذف یک گره، مثلا گره‌ی شماره‌ی 7، تمام سطوری که فیلد descendant آن‌ها در جدول TreePaths برابر با 7 است حذف می‌کنیم:
   DELETE FROM TreePaths WHERE descendant = 7;

برای حذف یک زیردرخت کامل، برای مثال گره‌ی شماره‌ی 4 و فرزندان (نوه‌های) آن، تمام سطوری که در جدول TreePaths دارای فیلد descendant با مقدار 4 هستند، حذف می‌کنیم. علاوه بر این باید نودهایی که به عنوان descendant به فیلد descendant گره‌ی 4، ارجاع داده می‌شوند نیز باید حذف گردد: 

DELETE FROM TreePaths
WHERE descendant IN (SELECT descendant
FROM TreePaths
WHERE ancestor = 4);

دقت کنید وقتی گره‌ای را حذف می‌کنیم، بدان معنی نیست که خود گره (نظر) را حذف می‌کنیم. البته این برای مثال نظر و پاسخ آن مقداری عجیب است ولی در مثال کارمندان در چارت سازمانی امری معمول است. هنگامی که ارتباطات یک کاربر را تغییر می‌دهیم، از حذف در جدول TreePaths استفاده می‌کنیم و این قضیه که ارتباطات کارمندان در جدول جداگانه‌ای ذخیره شده است به ما انعطاف‌پذیری بیشتری می‌دهد. 
برای جابجایی یک زیردرخت از مکانی به مکان دیگری در درخت، سطرهایی که ancestor گره‌ی بالایی زیردرخت را برمی‌گردانند و فرزندان آن گره را حذف می‌کنیم. برای مثال برای جابجایی گره‌ی شماره‌ی 6 به عنوان فرزند گره‌ی شماره‌ی 4 و قرار دادن آن به عنوان فرزند گره‌ی شماره‌ی 3، این چنین عمل می‌کنیم. فقط باید حواسمان جمع باشد سطری که گره‌ی شماره‌ی 6 به خودش ارجاع داده است را حذف نکنیم:
DELETE FROM TreePaths
WHERE descendant IN (SELECT descendant
                                         FROM TreePaths
                                         WHERE ancestor = 6)
AND ancestor IN (SELECT ancestor
                             FROM TreePaths
                             WHERE descendant = 6
                                 AND ancestor != descendant);

آن‌گاه این زیردرخت جدا شده را با اضافه کردن سطرهایی که با ancestor مکان جدید و descendant زیردرخت، منطبق هستند، به جدول اضافه می‌کنیم:
INSERT INTO TreePaths (ancestor, descendant)
SELECT supertree.ancestor, subtree.descendant
FROM TreePaths AS supertree
CROSS JOIN TreePaths AS subtree
WHERE supertree.descendant = 3
AND subtree.ancestor = 6;

روش Closure Table آسان‌تر از روش مجموعه‌های تودرتو است. هر دوی آن‌ها روش‌های سریع و آسانی برای ایجاد پرس‌وجو برای نیاکان و نوه‌ها دارند. ولی Closure Table برای نگهداری اطلاعات سلسله مراتب آسان‌تر است. در هر دو طراحی ایجاد پرس‌وجو در فرزندان و پدر بلافصل سرراست‌تر از روش‌ای لیست مجاورت و شمارش مسیر می‌باشد.
می‌توان عملکرد Closure Table را برای ایجاد پرس‌وجو روی فرزندان و پدر بلافصل را آسان‌تر نیز نمود. اگر فیلد path_length را به جدول TreePaths اضافه نماییم این کار انجام می‌شود. path_length گره‌ای که به خودش ارجاع می‌شود، صفر است. path_length فرزند بلافصل هر گره 1، path_length نوه‌ی آن 2 می‌باشد و به همین ترتیب path_lengthها را در هر سطر مقداردهی می‌کنیم. اکنون یا فتن فرزند گره‌ی شماره‌ی 4 آسان‌تر است:   
SELECT *
FROM TreePaths
WHERE ancestor = 4 AND path_length = 1;


از کدام طراحی استفاده نماییم؟
در این جا این سؤال مطرح است که ما باید از کدام طراحی استفاده نماییم. در پاسخ به این سؤال باید گفت که هر کدام از این روش‌ها نقاط قوت و ضعفی دارند که ما باید نسبت به عملیاتی که می‌خواهیم انجام دهیم از این طراحی‌ها استفاده کنیم. جدولی که در ادامه آمده است، مقایسه‌ای است میان میزان سهولت اجرای این طراحی‌ها در استفاده از پرس‌وجوهای متفاوت.

 لازم به ذکر است در اینجا ستون سوم (Query Child) به معنای پرس‌وجوهایی است که با فرزندان کار می‌کند و ستون چهارم  (Query Tree)  به معنای پرس‌وجوهایی است که با کل درخت کار می‌کنند، می‌باشد. 
نظرات مطالب
تفاوت‌های پروژه‌های ما و پروژه‌های اونا!
من اتفاقاً از همون روزهای اول در دسترس قرار گرفتن ویراستیار اون رو دریافت و نصب و تست کردم. واقعیتش اینه که به قضاوت من این کار، حاصل زحمت یک یا چند برنامه‌نویس تازه‌کار (اما خوش‌آتیه) است که در کارشون خیلی از نیازمندیهای اولیۀ چنین پروژه‌ای پیش‌بینی نشده. من به عنوان یه کاربر Word در گام اول دوست دارم غلطیابی به صورت همزمان با تایپ صورت بگیره (امکانی که خود ورد داره) و با تایپ هر کلمۀ غلط، ورد اون رو برام مشخص کنه، ویراستیار چنین کاری نمی‌کنه، امکاناتی که از طریق ویراستیار در دسترسه به درد مرور بعد از تایپ یا تصحیح سندهای موجود می‌خوره و حتی این کار رو هم (وقتی که قراره مثلاً کل سند را تصحیح کنه) به جای این که توی Thread مجزا انجام بده توی رشتۀ اصلی و با به حالت غیرپاسخگو بردن ورد انجام میده. پنجرۀ غلطیابیش حالت Modal داره و ... و نهایتاً این که بعد از نصبش (من با آفیس 2007 چک کردم) متوالیاً ورد کرش می‌کنه. از لحاظ داده‌ها هم که انتظار میره با یه مجموعۀ کامل روبرو باشیم من با همون تستهای اولیه متوجه نقصهای زیادی شدم.
من شخصاً ویراستیار رو در حد یه پروژۀ دانشجویی می‌پذیرم و نمرۀ 20 بهش میدم اما اعتقاد دارم انجام چنین کاری -جدا از فاز جمع‌آوری داده‌هاش- بدون این که اشکالات فعلی رو داشته باشه برای یه برنامه‌نویس متوسط چیزی شاید حدود یک هفته بیشتر کار روزانۀ 8 ساعته زمان نبره.
و اعتقاد دارم ساختن یه پروژۀ کاربردی غلطیابی می‌بایست با ارائۀ یه دیکشنری با قواعد نحوی برای hunspell یا پیاده‌سازی چنین قابلیتی مطابق استاندارد غلطیاب املایی خود آفیس انجام میشده.
مهمترین اشکال این پروژه اینه که یه سازمان دولتی خودش تصمیم گرفته پیاده‌سازیش کنه (و بعد همین سازمان میاد توی سایتش اعلام می‌کنه ما تمایل داریم پروژه‌ها رو برونسپاری کنیم اما ویراستیار به ما ثابت کرذ که بعضی کارها رو فقط ما خودمون می‌تونیم انجام بدیم!) و تمام هدفش -به نظر من- نه برآورده کردن یه نیاز که افتخارآفرینی برای خودشه.
مطالب
فعال سازی سطح دوم کش در Fluent NHibernate

سطح اول کش در NHibernate در یک تراکنش معنا پیدا می‌کند (+)؛ اما نتایج حاصل از اعمال سطح دوم (+) آن، در اختیار تمام تراکنش‌های جاری برنامه خواهند بود. در ادامه قصد داریم نحوه فعال سازی سطح دوم کش NHibernate را توسط Fluent NHibernate بررسی کنیم.

الف) دریافت کش پروایدر
برای این منظور به صفحه اصلی آن در سایت سورس فورج مراجعه نمائید(+). اگر به علت تحریم‌ها امکان دریافت فایل‌های مرتبط را نداشتید از این برنامه استفاده کنید(+). پس از دریافت، می‌خواهیم نحوه فعال سازی NHibernate.Caches.SysCache.dll را بررسی کنیم (این اسمبلی، در برنامه‌های وب و دسکتاپ بدون مشکل کار می‌کند).

ب) اعمال به قسمت تعاریف اولیه
پس از دریافت اسمبلی NHibernate.Caches.SysCache.dll و افزودن ارجاعی به آن، اکنون نوبت به معرفی آن به تنظیمات Fluent NHibernate‌ می‌باشد. این‌کار هم بسیار ساده است:
...
.ConnectionString(x => x.FromConnectionStringWithKey(...))
.Cache(x => x.UseQueryCache()
.UseMinimalPuts()
.ProviderClass<NHibernate.Caches.SysCache.SysCacheProvider>())
...

ج) تعریف نوع کش در هنگام ایجاد نگاشت‌ها
اگر از ClassMap‌ها برای تعریف نگاشت‌ها استفاده می‌کنید، در انتهای تعاریف یک سطر Cache.ReadWrite را اضافه کنید.
اگر از AutoMapping استفاده می‌کنید، نیاز است تا با استفاده از IAutoMappingOverride (+) سطر یاد شده اضافه گردد؛ برای مثال:
using FluentNHibernate.Automapping.Alterations;

namespace NH3Test.MappingDefinitions.Domain
{
public class AccountOverrides : IAutoMappingOverride<Account>
{
public void Override(FluentNHibernate.Automapping.AutoMapping<Account> mapping)
{
mapping.Cache.ReadWrite();
}
}
}
تعریف یک سطر فوق هم مهم است؛ زیرا در غیراینصورت فقط primary key حاصل از بار اول فراخوانی کوئری‌های مرتبط کش می‌شوند؛ نه نتیجه عملیات. هرچند این مورد هم یک قدم مثبت به شمار می‌رود از این لحاظ که برای مثال تهیه نتایج کوئری بر روی فیلدی که ایندکس بر روی آن تعریف نشده است همیشه از حالت تهیه کوئری بر روی فیلد دارای ایندکس کندتر است. اما هدف ما در اینجا این است که پس از بار اول فراخوانی کوئری، بار‌های دوم و بعدی دیگر کوئری خاصی را به بانک اطلاعاتی ارسال نکرده و نتایج از کش خوانده شوند (جهت استفاده عموم کاربران در کلیه تراکنش‌های جاری برنامه).

د) اعمال متد Cacheable به کوئر‌ی‌ها
سه مرحله قبل نحوه برپایی مقدماتی سطح دوم کش را بیان می‌کنند و تنها یکبار نیاز است انجام شوند. در ادامه هر جایی که نیاز داشتیم نتایج کوئری مورد نظر کش شوند (و باید دقت داشت که این کش شدن سطح دوم به معنی در دسترس بودن نتایج آن جهت تمام کاربران برنامه در تمام تراکنش‌های جاری برنامه هستند؛ برای مثال نتایج آمار سایت که دسترسی عمومی دارد) تنها کافی است متد Cacheable را به کوئری مورد نظر اضافه کرد؛ برای مثال:
var data = session.QueryOver<Account>()
.Where(s => s.Name == "name")
.Cacheable()
.List();

ه) چگونه صحت اعمال سطح دوم کش را بررسی کنیم؟
برای بررسی این امر باید به خروجی SQL نهایی مراجعه کرد (+). سه تراکنش مجزا را تعریف کنید. در تراکنش اول یک insert ساده، در تراکنش دوم کوئری از اطلاعات قبل (به همراه اعمال متد Cacheable) و در تراکنش سوم مجددا همان کوئری تراکنش دوم را (به همراه اعمال متد Cacheable) تکرار کنید. حاصل کار تنها باید دو عبارت SQL باشند. یک مورد جهت insert و یک مورد هم select . در تراکنش سوم، از نتایج کش شده تراکنش دوم استفاده خواهد شد؛ به همین جهت دیگری کوئری سومی به بانک اطلاعاتی ارسال نخواهد شد.
اگر اعمال مورد (ج) فوق را فراموش کنید، سه کوئری را مشاهده خواهید کرد، اما کوئری سوم با کوئری دوم اندکی متفاوت خواهد بود و بهینه‌تر؛ چون به صورت هوشمند بر اساس جستجوی بر روی primary key تغییر کرده است (صرفنظر از اینکه قسمت where کوئری شما چیست).

مطالب
مروری بر چند تجربه‌ی کاری با SQLite




SQLite بانک اطلاعاتی سریع، کم حجم و سورس بازی است که استفاده از آن در دات نت فریم ورک بسیار ساده است. فقط کافی است پروایدر مربوط به آن را دریافت کنید و در کدهای قدیمی خود هر جایی مثلا sqlconnection داشتید آن‌را تبدیل به sqliteconnection کنید و امثال آن (به بیان دیگر، پروایدر تهیه شده از معماری ADO.NET پیروی می‌کند و عملا دانش قبلی شما به سادگی قابل استفاده و ارتقاء است). علاوه بر آن پروایدر ADO.NET‌ تهیه شده برای آن، پشتیبانی از Entity framework را هم ارائه می‌دهد.
این دیتابیس تحت سیستم عامل‌های مختلف مهیا است و مهم‌ترین مزیت آن عدم نیاز به نصب آن می‌باشد.

مزایا:
- سورس باز و رایگان
- مهیا بودن آن در سایر پلتفرم‌ها (ویندوز، لینوکس و ...)
- نیازی به نصب ندارد و فقط یک DLL بومی است. این مورد برای کاربرانی که در مدیریت بانک‌های اطلاعاتی پیچیده مشکل دارند، یک مزیت مهم است.
- امکان تشیکل دیتابیس در حافظه. این نکته و توانایی، در آزمون‌های واحد بسیار جالب توجه است. می‌توانید با سرعت بالا دیتابیسی واقعی را در حافظه تشکیل داده، کلیه آزمون‌های واحد خود را اجرا کرده و پس از پایان کار، اثری از دیتابیس و تغییر داده‌ها و مشکلات بازگردانی اطلاعات به حالت اول وجود نخواهد داشت.


ملاحظات:
الف) مرتب سازی SQLite حساس به حروف کوچک و بزرگ است.
برای برگشت به عادت متداولی که وجود دارد می‌شود به صورت زیر عمل کرد:
select f1 from tbl1 order by f1 COLLATE NOCASE
یک COLLATE NOCASE اضافه شده است.

ب) رعایت نکات مرتبط با سیستم‌های 64 بیتی
در مورد سیستم‌های 64 بیتی و دات نت قبلا مطلبی را نوشته بودم : {+}. این مطلب دقیقا اینجا کاربرد پیدا می‌کند، از این لحاظ که SQLite یک بانک اطلاعاتی Native است. اگر برنامه‌ی دات نت شما برای حالت Any CPU تهیه شده است، در سیستم‌های 32 بیتی نیاز است تا DLL مرتبط SQLite را توزیع کنید و در سیستم‌های 64 بیتی DLL مرتبط 64 بیتی آن نیاز خواهد بود. در غیراینصورت برنامه‌ی شما در بدو امر کرش کرده و اجرا نخواهد شد.

مشکلات:
الف) کلید خارجی بی خاصیت!
SQLite از کلید خارجی پشتیبانی می‌کند اما آن‌را اعمال نمی‌کند! برای اینکه کلید خارجی را اعمال کنید باید خودتان تریگر بنویسید تا این‌کار را انجام دهد.

ب) پشتیبانی در حد صفر از مباحث همزمانی و تردینگ.
اگر برنامه شما مالتی ترد است، در بد مخمصه‌ای گرفتار شده‌اید. مدام با پیغام database is locked مواجه خواهید شد. (چه انتظاری داشتید؟ یک dll کمتر از 2 مگابایت که قرار نیست کار غول‌های دیتابیسی را انجام دهد)
بنابراین اصلا تصورش را هم نکنید که از این دیتابیس به عنوان بانک اطلاعاتی یک سایت (و محیط‌های چند کاربره) بتوان استفاده کرد و کاربران دچار مشکل نشوند.

ج)حجم بالای دیتابیس و عدم کش
از مباحث caching که در دیتابیس‌های معظم دیگر به صورت توکار وجود دارد خبری نیست. برای مثال اگر یک کوئری قرار است تعدادی را شمارش نماید، حاصلی کش نشده و اگر صدبار هم به صورت متوالی آن‌را فراخوانی کنید باز هم از نو محاسبات آن انجام خواهد شد.
این مورد در حجم بالای دیتابیس واقعا مهم است و نمودش را با دیتابیسی با حجم بالای یک گیگ به وضوح مشاهده خواهید. افت کارآیی و همچنین قرچ و قرچ مداوم هارد دیسک سیستم! (چون به کش رجوع نمی‌شود)

د) امنیت
روی بانک‌های اطلاعاتی اکسس حداقل می‌توان یک کلمه‌ی عبور را قرار داد (که در کسری از ثانیه قابل شکستن است!). در SQLite استاندارد هیچ خبری از این مباحث نبوده و امنیت را باید خودتان تامین کنید. (البته یک نسخه‌ی تجاری هم از این بانک اطلاعاتی با پشتیبانی از رمزنگاری اطلاعات موجود است : +)

ه) مرتب سازی فارسی
هر چند SQLite هیچ مشکلی در ثبت اطلاعات یونیکد و خصوصا متون فارسی ندارد، اما با مرتب سازی کلمات یونیکد مشکل داشته و بر اساس کد اسکی آن‌ها عمل می‌کند. هر چند امکان تعریف Collation سفارشی در آن ممکن است : + (البته ممکن بودن با موجود بودن متفاوت است)


نتیجه گیری:
- SQLite برای دیتابیسی تا حدود یک گیگ که فقط یک نفر قرار باشد از آن استفاده کند انتخاب بسیار مناسبی است (برای مثال فایرفاکس از آن برای ذخیره سازی تنظیمات خودش استفاده می‌کند).