$(document).ready(function () { $(document).on('click', '.confirm', function () { alert("Clicked Me !"); }); });
حال فرض کنید در یکی از صفحات قصد داریم اگر بر دکمهای که قبلا برای آن رویدادی نوشتهایم (منظور کدهای بالا میباشد)، کلیک شد، یکسری عملیات دیگر، جدای از آن عملیات انجام شود. برای اینکار در صفحه مربوطه، کدی شبیه زیر را نوشتهایم :
$(document).ready(function () { $(document).on('click', '.confirm', function () { alert("Clicked Me On nested Page !"); }); });
اگر پروژه را اجرا نمایید و بر روی دکمه مربوطه کلیک نماییم، هر دو Event نوشته شده، اجرا خواهند شد. در چنین حالتی تکلیف چیست و چکار باید کرد؟ جواب: selector را تغییر دهیم :خیر.
برای این کار میتوان رویداد کلیک را برای تگ مورد نظر با استفاده از متد off بازنویسی کنیم:
$(document).off('click', '.confirm');
- روشهایی دیگر برای انجام این کار:
$(".confirm").removeAttr("onclick"); $( ".confim").unbind( "click" );
معرفی کتابخانهی DNTCaptcha.Core
الف) دریافت TopShelf
TopShelf یک کتابخانه سورس باز است و علاوه بر آن، آخرین فایلهای باینری آنرا از طریق نیوگت نیز میتوان دریافت کرد:
PM> Install-Package Topshelf
ب) فعال سازی TopShelf
یک برنامه ساده کنسول را ایجاد کنید. سپس با استفاده از نیوگت و اجرای فرمان فوق، ارجاعی را به اسمبلی TopShelf اضافه نمائید.
using Topshelf; namespace MyService { class Program { static void Main(string[] args) { HostFactory.Run(config => { config.Service(settings => new TestService()); config.EnableServiceRecovery(recovery => recovery.RestartService(delayInMinutes: 1)); config.EnableShutdown(); config.EnablePauseAndContinue(); config.SetDescription("MyService Desc."); config.SetDisplayName("MyService"); config.SetServiceName("MyService"); config.RunAsLocalSystem(); }); } } }
using Topshelf; using Topshelf.Logging; namespace MyService { public class TestService : ServiceControl { static readonly LogWriter _log = HostLogger.Get<TestService>(); public bool Start(HostControl hostControl) { _log.Info("TestService Starting..."); return true; } public bool Stop(HostControl hostControl) { _log.Info("TestService Stopped"); return true; } public bool Pause(HostControl hostControl) { _log.Info("TestService Paused"); return true; } public bool Continue(HostControl hostControl) { _log.Info("TestService Continued"); return true; } } }
ج) نصب TopShelf
در همین حالت اگر برنامه را اجرا کنید، سرویس ویندوز ان تی تهیه شده، شروع به کار خواهد کرد (مزیت مهم آن نسبت به قالب توکار تهیه سرویسهای ویندوز در ویژوال استودیو).
برای نصب این سرویس تنها کافی است در خط فرمان با دسترسی مدیریتی، دستور نصب your_exe install و یا عزل your_exe uninstall صادر شوند.
پیشتر در مورد ELMAH مطلبی را منتشر کرده بودم و اگر برنامه نویس ASP.NET هستید و با ELMAH آشنایی ندارید، جدا نیمی از عمر کاری شما بر فنا است!
هاست پیش فرض یک WCF RIA Service هم یک برنامهی ASP.NET است. بنابراین کلیهی خطاهای رخ داده در سمت سرور را باید بتوان به نحوی لاگ کرد تا بعدا با مطالعهی آنها اطلاعات ارزشمندی را از نقایص برنامه در عمل و پیش از گوشزد شدن آنها توسط کاربران، دریافت، بررسی و رفع کرد.
کلیه خطاها را لاگ میکنم تا:
- بدانم معنای جملهی "برنامه کار نمیکنه" چی هست.
- بدون روبرو شدن با کاربران یا حتی سؤال و جوابی از آنها بدانم دقیقا مشکل از کجا ناشی شده.
- بدانم رفتارهای عمومی کاربران که منجر به بروز خطا میشوند کدامها هستند.
- بدانم در کدامیک از قسمتهای برنامه تعیین اعتبار ورودی کاربران یا انجام نشده یا ضعیف و ناکافی است.
- بدانم زمانیکه دوستی (!) قصد پایین آوردن برنامه را با تزریق SQL داشته، دقیقا چه چیزی را وارد کرده، در کجا و چه زمانی؟
- بتوانم Remote worker خوبی باشم.
ELMAH هم برای لاگ کردن خطاهای مدیریت نشدهی یک برنامهی ASP.NET ایجاد شده است. بنابراین باید بتوان این دو (WCF RIA Services و ELMAH) را به نحوی با هم سازگار کرد. برای اینکار نیاز است تا یک مدیریت کنندهی خطای سفارشی را با پیاده سازی اینترفیس IErrorHandler تهیه کنیم (تا خطاهای مدیریت نشدهی حاصل را به سمت ELMAH هدایت کند) و سپس آنرا به کمک یک ویژگی یا Attribute به DomainService خود جهت لاگ کردن خطاها اعمال نمائیم. روش تعریف این Attribute را در کدهای بعد ملاحظه خواهید نمود (در اینجا نیاز است تا دو ارجاع را به اسمبلیهای Elmah.dll که دریافت کردهاید و اسمبلی استاندارد System.ServiceModel نیز به پروژه اضافه نمائید):
//add a reference to "Elmah.dll"
using System;
using System.ServiceModel.Channels;
using System.ServiceModel.Dispatcher;
using System.Web;
namespace ElmahWcf
{
public class HttpErrorHandler : IErrorHandler
{
#region IErrorHandler Members
public bool HandleError(Exception error)
{
return false;
}
public void ProvideFault(Exception error, MessageVersion version, ref Message fault)
{
if (error == null)
return;
if (HttpContext.Current == null) //In case we run outside of IIS
return;
Elmah.ErrorSignal.FromCurrentContext().Raise(error);
}
#endregion
}
}
//add a ref to "System.ServiceModel" assembly
using System;
using System.Collections.ObjectModel;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
namespace ElmahWcf
{
public class ServiceErrorBehaviorAttribute : Attribute, IServiceBehavior
{
Type errorHandlerType;
public ServiceErrorBehaviorAttribute(Type errorHandlerType)
{
this.errorHandlerType = errorHandlerType;
}
#region IServiceBehavior Members
public void AddBindingParameters(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase, Collection<ServiceEndpoint> endpoints,
BindingParameterCollection bindingParameters)
{ }
public void ApplyDispatchBehavior(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{
IErrorHandler errorHandler;
errorHandler = (IErrorHandler)Activator.CreateInstance(errorHandlerType);
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher cd = cdb as ChannelDispatcher;
cd.ErrorHandlers.Add(errorHandler);
}
}
public void Validate(
ServiceDescription serviceDescription,
ServiceHostBase serviceHostBase)
{ }
#endregion
}
}
[ServiceErrorBehavior(typeof(HttpErrorHandler))] //Integrating with ELMAH
[EnableClientAccess()]
public partial class MyDomainService : LinqToEntitiesDomainService<myEntities>
در ادامه نحوهی افزودن تعاریف متناظر با ELMAH به Web.Config برنامه ذکر شده است. این تعاریف برای IIS6 و 7 به بعد هم تکمیل گردیده است. خطاها هم به صورت فایلهای XML در پوشهای به نام Errors که به ریشهی سایت اضافه خواهید نمود (یا هر پوشهی دلخواه دیگری)، لاگ میشوند.
به نظر من این روش، از ذخیره سازی اطلاعات لاگها در دیتابیس بهتر است. چون اساسا زمانیکه خطایی رخ میدهد شاید مشکل اصلی همان ارتباط با دیتابیس باشد.
قسمت ارسال خطاها به صورت ایمیل نیز comment شده است که در صورت نیاز میتوان آنرا فعال نمود:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
<section name="errorTweet" requirePermission="false" type="Elmah.ErrorTweetSectionHandler, Elmah"/>
</sectionGroup>
</configSections>
<elmah>
<security allowRemoteAccess="1" />
<errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/Errors" />
<!-- <errorMail
from="errors@site.net"
to="nasiri@site.net"
subject="prj-error"
async="true"
smtpPort="25"
smtpServer="mail.site.net"
noYsod="true" /> -->
</elmah>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="DomainServiceModule"
preCondition="managedHandler"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</modules>
<validation validateIntegratedModeConfiguration="false" />
<handlers>
<add name="Elmah" verb="POST,GET,HEAD" path="myelmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</handlers>
</system.webServer>
<system.web>
<globalization
requestEncoding="utf-8"
responseEncoding="utf-8"
/>
<authentication mode="Forms">
<!--one month ticket-->
<forms name=".403AuthV"
cookieless="UseCookies"
slidingExpiration="true"
protection="All"
path="/"
timeout="43200" />
</authentication>
<httpHandlers>
<add verb="POST,GET,HEAD" path="myelmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
</httpHandlers>
<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
<add name="DomainServiceModule"
type="System.ServiceModel.DomainServices.Hosting.DomainServiceHttpModule, System.ServiceModel.DomainServices.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>
<compilation debug="true" targetFramework="4.0">
<assemblies>
<add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</assemblies>
</compilation>
</system.web>
<connectionStrings>
</connectionStrings>
<system.serviceModel>
<serviceHostingEnvironment
aspNetCompatibilityEnabled="true"
multipleSiteBindingsEnabled="true" />
</system.serviceModel>
</configuration>
throw new Exception("This is an ELMAH test");
سپس به آدرس http://localhost/myelmah.axd مراجعه نموده و اطلاعات لاگ شده حاصل را بررسی کنید:
این روش با WCF Services های متداول هم کار میکند. فقط در این سرویسها باید aspNetCompatibilityEnabled مطابق تگهای ذکر شدهی system.serviceModel فوق در web.config لحاظ شوند (این مورد به صورت پیش فرض در WCF RIA Services وجود دارد). همچنین ویژگی زیر نیز باید به سرویس شما اضافه گردد:
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)]
منابع مورد استفاده:
Integrating ELMAH for a WCF Service
Making WCF and ELMAH play nice together
Getting ELMAH to work with WCF services
پ.ن.
اگر به خطاهای ASP.NET دقت کرده باشید که به yellow screen of death هم مشهور هستند (در مقابل صفحات آبی ویندوز!)، ابتدای آن خیلی بزرگ نوشته شده Server Error و سپس ادامهی خطا. همین مورد دقیقا یادم هست که هر بار سبب بازخواست مدیران شبکه بجای برنامه نویسها میشد! (احتمالا این هم یک نوع بدجنسی تیم ASP.NET برای گرفتن حال ادمینهای شبکه است! و گرنه مثلا میتوانستند همان ابتدا بنویسند program/application error بجای server error)
سایت iplocationtools بانک اطلاعاتی موقعیت مکانی IP های خود را که ماهیانه نیز به روز خواهد شد، به رایگان برای دریافت قرار داده است. فرمت تعاریف جداول و عبارت insert آن مطابق دستورات بانک اطلاعاتی MySQL است.
تنها بازیگر شاخص یازده سال قبل در زمینه توسعه برنامههای مبتنی بر QT ، برنامه KDevelop بوده است. این وضعیت با ارائه Qt Creator بهبود یافته است ...
- شرکت Red gate که پیشتر برنامهی معروف Reflector را در جهت اهداف تبلیغاتی خود تصاحب کرده بود، اکنون در ادامهی همین سیاست، کتاب Illustrated C# 2008 را نیز به رایگان ارائه داده است.
- سایت DelphiFeeds.com با طراحی جدید خود چند روزی است که متحول شده است. سایتی شبیه به سایت مهندس خودمان که بعد از عید معلوم نیست چه بلایی سرش آمده است و جای خالی آن احساس میشود، هر چند سایت IDevCenter نیز همین کار را به صورت تخصصیتری ارائه میدهد.
- Microsoft MED-V 1.0 در جهت توسعهی اهداف مجازی سازی مایکروسافت ارائه شد.
//برای ذخیره مقادیر از ساختار نام و مقدار استفاده میکنیم که نامها را اینجا ثبت کرده ام var Variables={ posts:"posts", postsComments:"postsComments", shares:"shares", sharesComments:"sharesComments", } //برای ذخیره زمان آخرین تغییر سایت برای هر یک از مطالب به صورت جداگانه نیاز به یک ساختار نام و مقدار است که نامها را در اینجا ذخیره کرده ام var DateContainer={ posts:"dtposts", postsComments:"dtpostsComments", shares:"dtshares", sharesComments:"dtsharesComments", interval:"interval" } //برای نمایش پیامها به کاربر var Messages={ SettingsSaved:"تنظیمات ذخیره شد", SiteUpdated:"سایت به روز شد", PostsUpdated:"مطلب ارسالی جدید به سایت اضافه شد", CommentsUpdated:"نظری جدیدی در مورد مطالب سایت ارسال شد", SharesUpdated:"اشتراک جدید به سایت ارسال شد", SharesCommentsUpdated:"نظری برای اشتراکهای سایت اضافه شد" } //لینکهای فید سایت var Links={ postUrl:"https://www.dntips.ir/feeds/posts", posts_commentsUrl:"https://www.dntips.ir/feeds/comments", sharesUrl:"https://www.dntips.ir/feed/news", shares_CommentsUrl:"https://www.dntips.ir/feed/newscomments" } //لینک صفحات سایت var WebLinks={ Home:"https://www.dntips.ir", postUrl:"https://www.dntips.ir/postsarchive", posts_commentsUrl:"https://www.dntips.ir/commentsarchive", sharesUrl:"https://www.dntips.ir/newsarchive", shares_CommentsUrl:"https://www.dntips.ir/newsarchive/comments" }
chrome.runtime.onInstalled.addListener(function(details) { var now=String(new Date()); var params={}; params[Variables.posts]=true; params[Variables.postsComments]=false; params[Variables.shares]=false; params[Variables.sharesComments]=false; params[DateContainer.interval]=1; params[DateContainer.posts]=now; params[DateContainer.postsComments]=now; params[DateContainer.shares]=now; params[DateContainer.sharesComments]=now; chrome.storage.local.set(params, function() { if(chrome.runtime.lastError) { /* error */ console.log(chrome.runtime.lastError.message); return; } }); });
chrome.storage.local.set('mykey':myvalue,....
chrome.storage.local.set(mykey:myvalue,...
"background": { "scripts": ["const.js","init.js"] }
نکته:نمی توان در تعریف بک گراند هم فایل اسکریپت معرفی کرد و هم فایل html
"background": { "page": "background.htm" }
<html> <head> <script type="text/javascript" src="const.js"></script> <script type="text/javascript" src="https://www.google.com/jsapi"></script> <script type="text/javascript" src="init.js"></script> <script type="text/javascript" src="omnibox.js"></script> <script type="text/javascript" src="rssreader.js"></script> <script type="text/javascript" src="contextmenus.js"></script> </head> <body> </body> </html>
- کدنویسی راحتتر و خلاصهتر برای خواندن RSS
- استفاده اجباری از یک پروکسی به خاطر Content Security Policy و حتی CORS
"content_security_policy": "script-src 'self' https://*.google.com; object-src 'self'"
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
استفاده از این Api در rssreader.js
google.load("feeds", "1"); google.setOnLoadCallback(alarmManager);
function alarmManager() { chrome.storage.local.get(DateContainer.interval,function ( items) { period_time==items[DateContainer.interval]; chrome.alarms.create('RssInterval', {periodInMinutes: period_time}); }); chrome.alarms.onAlarm.addListener(function (alarm) { console.log(alarm); if (alarm.name == 'RssInterval') { var boolposts,boolpostsComments,boolshares,boolsharesComments; chrome.storage.local.get([Variables.posts,Variables.postsComments,Variables.shares,Variables.sharesComments],function ( items) { boolposts=items[Variables.posts]; boolpostsComments=items[Variables.postsComments]; boolshares=items[Variables.shares]; boolsharesComments=items[Variables.sharesComments]; chrome.storage.local.get([DateContainer.posts,DateContainer.postsComments,DateContainer.shares,DateContainer.sharesComments],function ( items) { var Vposts=new Date(items[DateContainer.posts]); var VpostsComments=new Date(items[DateContainer.postsComments]); var Vshares=new Date(items[DateContainer.shares]); var VsharesComments=new Date(items[DateContainer.sharesComments]); if(boolposts){var result=RssReader(Links.postUrl,Vposts,DateContainer.posts,Messages.PostsUpdated);} if(boolpostsComments){var result=RssReader(Links.posts_commentsUrl,VpostsComments,DateContainer.postsComments,Messages.CommentsUpdated); } if(boolshares){var result=RssReader(Links.sharesUrl,Vshares,DateContainer.shares,Messages.SharesUpdated);} if(boolsharesComments){var result=RssReader(Links.shares_CommentsUrl,VsharesComments,DateContainer.sharesComments,Messages.SharesCommentsUpdated);} }); }); } }); }
- آدرس فیدی که قرار است از روی آن بخواند
- آخرین به روزسانی که از سایت داشته متعلق به چه تاریخی است.
- نام کلید ذخیره سازی تاریخ آخرین تغییر سایت که اگر بررسی شد و مشخص شد سایت به روز شده است، تاریخ جدید را روی آن ذخیره کنیم.
- در صورتی که سایت به روز شده باشد نیاز است پیامی را برای کاربر نمایش دهیم که این پیام را در اینجا قرار میدهیم.
function RssReader(URL,lastupdate,datecontainer,Message) { var feed = new google.feeds.Feed(URL); feed.setResultFormat(google.feeds.Feed.XML_FORMAT); feed.load(function (result) { if(result!=null) { var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent; var RssUpdate=new Date(strRssUpdate); if(RssUpdate>lastupdate) { SaveDateAndShowMessage(datecontainer,strRssUpdate,Message) } } }); }
var strRssUpdate = result.xmlDocument.firstChild.firstChild.childNodes[5].textContent;
function SaveDateAndShowMessage(DateField,DateValue,Message) { var params={ } params[DateField]=DateValue; chrome.storage.local.set( params,function(){ var options={ type: "basic", title: Messages.SiteUpdated, message: Message, iconUrl: "icon.png" } chrome.notifications.create("",options,function(){ chrome.notifications.onClicked.addListener(function(){ chrome.tabs.create({'url': WebLinks.Home}, function(tab) { }); }); }); }); }
"permissions": [ "storage", "tabs", "alarms", "notifications" ]
chrome.notifications.create("",options,function(){ chrome.notifications.onClicked.addListener(function(){ chrome.tabs.create({'url': WebLinks.Home}, function(tab) { });}); });
"web_accessible_resources": [ "icon.png" ]
خوب؛ کار افزونه تمام شده است ولی اجازه دهید این بار امکانات افزونه را بسط دهیم:
"options_page": "popup.html"
جایزگزینی صفحات یا Override Pages
"chrome_url_overrides": { "newtab": "newtab.htm" }
ایجاد یک تب اختصاصی در Developer Tools
"devtools_page": "devtools.htm"
<script src="devtools.js"></script>
chrome.devtools.panels.create( "Dotnettips Updater Tools", "icon.png", "devtoolsui.htm", function(panel) { } );
- One-Time Requests یا درخواستهای تک مرتبهای
- Long-Lived Connections یا اتصالات بلند مدت یا مصر
درخواستهای تک مرتبه ای
window.addEventListener("load", function() { chrome.extension.sendMessage({ type: "dom-loaded", data: { myProperty : "value" } }); }, true);
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) { switch(request.type) { case "dom-loaded": alert(request.data.myProperty ); break; } return true; });
var port = chrome.runtime.connect({name: "my-channel"}); port.postMessage({myProperty: "value"}); port.onMessage.addListener(function(msg) { // do some stuff here });
chrome.runtime.onConnect.addListener(function(port) { if(port.name == "my-channel"){ port.onMessage.addListener(function(msg) { // do some stuff here }); } });
نمونه کد
آپلود نهایی کار در Google web store
برای آپلود نهایی کار به google web store که در آن تمامی برنامهها و افزونههای کروم قرار دارند بروید. سمت راست آیکن تنظیمات را بزنید و گزینه developer dashboard را انتخاب کنید تا صفحهی آپلود کار برای شما باز شود. دایرکتوری محتویات اکستنشن را zip کرده و آپلود نمایید. توجه داشته باشید که محتویات و سورس خود را باید آپلود کنید نه فایل crx را. بعد از آپلود موفقیت آمیز، صفحهای ظاهر میشود که از شما آیکن افزونه را در اندازه 128 پیکسل میخواهد بعلاوه توضیحاتی در مورد افزونه، قیمت گذاری که به طور پیش فرض به صورت رایگان تنظیم شده است، لینک وب سایت مرتبط، لینک محل پرسش و پاسخ برای افزونه، اگر لینک یوتیوبی در مورد افزونه دارید، یک شات تصویری از افزونه و همینطور چند تصویر برای اسلایدشو سازی که در همان صفحه استاندارد آنها را توضیح میدهد و در نهایت گزینهی جالبتر هم اینکه اکستنشن شما برای چه مناطقی تهیه شده است که متاسفانه ایران را ندیدم که میتوان همه موارد را انتخاب کرد. به خصوص در مورد ایران که آی پیها هم صحیح نیست، انتخاب ایران چنان تاثیری ندارد و در نهایت گزینهی publish را میزنید که متاسفانه بعد از این صفحه درخواست میکند برای اولین بار باید 5 دلار آمریکا پرداخت شود که برای بسیاری از ما این گزینه ممکن نیست.
سورس پروژه را میتوانید از اینجا ببینید و خود افزونه را از اینجا دریافت کنید.
استفاده یا عدم استفاده از یک تکنولوژی یا ابزار خاص، به پارامترهای مختلفی از جمله ابعاد پروژه، مهارت و دانش اعضای تیم، ماهیت پروژه، پلتفرم اجرا، بودجهی پروژه، مهلت تکمیل پروژه و تعداد نفرات تیم بستگی دارد. بنابراین واضح است پیچیدن یک نسخهی خاص، برای همهی سناریوها امکان پذیر نیست؛ اما شرایطی وجود دارد که استفاده یا عدم استفاده از این ابزارهای تکنولوژیک منطقیتر مینمایند.
Stored Procedure (که از این به بعد برای ایجاز، SP نوشته خواهد شد) هم از قاعده فوق مستثنی نیست و در صورت انتخاب صحیح میتواند به ارائهی محصول نهایی با کیفیتتری در زمان کوتاهتری کمک کند و در صورت انتخاب ناآگاهانه ممکن است باعث شکست یک پروژه (بخصوص در بلند مدت) شود.
تاریخچه
SQL توسط شرکت IBM در اوایل دهه 70 میلادی ایجاد شد. با اوج گرفتن زبانهای رویهای، SQL هم چندان از این قافله عقب نماند که منجر به پذیرش SP به عنوان یک استاندارد، در دهه 90 میلادی و پیاده سازی تدریجی آن توسط غولهای سازنده دیتابیس شد (رجوع فرمایید به ^ و ^). این فاصله 20 ساله باعث غنیتر شدن SQL شد و وجود SP - به معنی انتقال مدل برنامه نویسی رویهای به SQL - بخشی از مشکلات قبلی کار با کوئریهای پشت سر هم و خام را حل کرد. از سال 2000 میلادی به بعد، ORMهای قدرتمندی از جمله Hibernate و پیاده سازیهای مختلفی از Active Record و Entity Framework متولد شدند. بنابر این تقدم و تاخّرهای زمانی، بدیهی است اغلب مزایای SP نسبت به Raw SQL Query و اغلب معایب آن نسبت به ORMها باشد.
بنظر میرسد برای پاسخ به سوال اصلی این مطلب، ناگزیر به مقایسه SP با رقبای دیرینهاش هستیم. با برشمردن معایب و مزایای SP میتوان به نتیجهی منطقیتری رسید. البته باید در نظر داشت صرف استفاده از SP به معنای بهرهمند شدن از مزایای آن و صرف استفاده نکردن از آن هم بهرهمندی از رقبای آن نیست. چگونگی استفاده یک ابزار، مهمتر از خود ابزار است.
معایب SP
- دستورات Alter Table ، Add Column و Drop Column به این سادگیها هم نیستند؛ ممکن است به یکی از جداول دیتابیس دو ستون اضافه یا از آن حذف شوند. مجبوریم تمامی SPها را بخصوص Insert و Update متناظر با جدول را تغییر دهیم که این تغییرات ممکن است بصورت زنجیرهوار به سایر SPها هم سرایت کند. حال شرایطی را در نظر بگیرید که تعداد SPهای شما به چند ده و یا حتی به چند صد عدد و بیشتر، رسیده باشد که این به معنی زحمت بیشتر و تغییرات پر هزینهتر است.
- احتمال کند شدن ماشین سرویس دهنده در اثر اجرای تعداد
زیادی SP ؛ چناچه بخش زیادی از منطق برنامه از طریق SP اجرا شود، سرور دیتابیس موظف به اجرای آنهاست. اما در صورتیکه منطق،
در کد برنامه قرار داشته باشد، امکان توزیع آن بر روی سرورهای مجزا و یا حتی ماشین
کلاینت وجود خواهد داشت. امروزه اکثر کلاینتها به دیتابیسهای سبک و سریعی مجهز شدهاند. بنابراین در صورت امکان چرا بار پردازشی را به عهده آنها نگذاریم؟!
- یکپارچگی کمتر؛ تقریبا همه اپلیکیشنها نیازمند
ارتباط با سایر سیستمها هستند. اگر بخشهای زیادی از منطق برنامه درون SP مخفی شده باشند، این نقطه تلاقی بین سیستمی، احتمالا
درون خود دیتابیس قرار میگیرد و این به معنی ایجاد SP های بیشتر، افزودن
پارامترهای بیشتر، توسعه SPهای قبلی و بطور
خلاصه اعمال تغییرات بیشتر، که منتج به قابلیت نگهداری کمترخواهد شد.
- انعطاف پذیری کمتر؛ در یک شرایط ایده آل، عملکرد اپلیکیشن، مستقل از دیتابیس است. اگر نیاز به تغییر دیتابیس، مثلا از اوراکل به Microsoft SQL Server وجود داشته باشد، نیاز به بازنویسی و انتقال فانکشنها و SP ها محتمل است و از آنجائیکه که با وجود استانداردها، دیتابیسهای مختلف، معمولا در Syntax دستورات، تفاوتهای فاحشی دارند، هر چه کد بیشتری در SP ها باشد، نیاز به انتقال و تبدیل بیشتری وجود دارد.
- عدم وجود بازخورد مناسب؛ بسیاری از اوقات در صورت بروز اشکالی در حین اجرای یک SP، فقط با یک متن ساده بصورت Table has no rows و یا error مواجه میشویم. چنین خطاهایی هنگام دیباگ اصلا خوشایند نیستند. MS SQL در این بین بازخوردهای مناسبی را ارائه میکند. اگر تجربه کار با سایر دیتابیسها را داشته باشید، اهمیت بازخوردهای مناسب، ملموستر خواهد بود.
- کد نویسی سختتر؛ نوشتن کد SQL معمولا در همان IDE اپلیکیشن انجام نمیشود. جابجایی مداوم بین دو IDE ، دیباگ و کد نویسی از طریق دو اینترفیس مجزا، اصلا ایدهال نیست.
- SP منطق را بیش از حد پنهان میکند؛ حتی با دانستن نام صحیح یک SP، باز هم تصویری از پارامترهای ارسالی به آن و نتیجه برگشتی نخواهیم داشت. نمیدانیم نتیجه حاصل از اجرای SP ما مقداری را برمیگرداند یا خیر؟ در صورت وجود برگشتی، یک Cursor است یا یک مقدار؟ اگر Cursor است شامل چه ستونهایی است؟
- SP نمیتواند یک شیء را به عنوان آرگومان بپذیرد؛ بنابراین احتمال کثیف شدن کد به مرور افزایش پیدا میکند و بدتراز آن، در صورت ارسال اشتباه یک پارامتر، یا عدم تطابق تعداد پارامترها، مجبور به بررسی تمام آنها بصورت دستی هستیم. برای مثال دو قطعه کد زیر را با هم مقایسه کنید:
INSERT INTO User_Table(Id,Username,Password,FirstName,SureName,PhoneNumber,x,Email) VALUES (1,'VahidN','123456','Vahid','Nasiri','09120000000','vahid_xxx@example.com')
و معادل آن در یک ORM فرضی:
public void Insert(User user) { _users.Insert(user); db.Save(); }
بهوضوح قطعه کد sql، قبل از خوب یا بد بودن، زشت است. همچنین پارامتر x آن که فرضاً به تازگی اضافه شده، مقداری را دریافت نکرده و باعث بروز خطا خواهد شد.
- نبود Query Chaining؛ یکی از ویژگیهای جذاب ORMهای امروزی، امکان تشکیل یک کوئری با قابلیت خوانایی بالا و افزودن شرطهای بیشتر از طریق الگوی builder است. قطعه کد زیر یک SP برای جستجوی داینامیک نام و نام خانوادگی در یک جدول فرضی به اسم Users است:
public ICollection<User> GetUsers(string firstName,string lastName,Func<User, bool> orderBy) { var query = _users.where(u => u.LastName.StartsWith(lastName)); query = query.where(u => u.FirstName.StartsWith(firstName)); query = query.OrderBy(orderBy); return query.ToList(); }
در مقایسه با معادل SP آن:
CREATE PROCEDURE DynamicWhere @LastName varchar(50) = null, @FirstName varchar(50) = null, @Orderby varchar(50) = null AS BEGIN DECLARE @where nvarchar(max) SELECT @where = '1 = 1' IF @LastName IS NOT NULL SELECT @Where = @Where + " AND A.LastName LIKE @LastName + '%'" IF @FirstName IS NOT NULL SELECT @Where = @Where + " AND A.FirstName LIKE @FirstName + '%'" DECLARE @orderBySql nvarchar(max) SELECT @orderBySql = CASE WHEN @OrderBy = "LastName" THEN "A.LastName" ELSE @OrderBy = "FirstName" THEN "A.FirstName" END DECLARE @sql nvarchar(max) SELECT @sql = " SELECT A.Id , A.AccountNoId, A.LastName, A.FirstName, A.PostingDt, A.BillingAmount FROM Users WHERE " + @where + " ORDER BY " + @orderBySql exec sp_executesql @sql, N'@LastName varchar(50), @FirstName varchar(50) @LastName, @FirstName END
حاجت به گفتن نیست که قطعه کد اول چقدر خواناتر، انعطاف پذیرتر، خلاصهتر و قابل نگهداریتر است.
- نداشتن امکانات زبانهای مدرن؛ زبانها و IDEهای مدرن، امکانات قابل توجهی را برای نگهداری بهتر، انعطاف پذیری بیشتر، مقیاس پذیری بالاتر، تست پذیری دقیقتر و... ارائه میکنند. به عنوان مثال:
- شیءگرایی و امکانات آن که در SP موجود نیست و در مورد قبلی معایب، به آن مختصرا اشاره شد. در نظر بگیرید اگر SQL زبانی شیء گرا بود و مجهز به ارث بری و کپسوله سازی بود، چقدر قابلیت نگهداری آن بالاتر میرفت و حجم کدهای نوشته شده میتوانست کمتر باشند.
- نداشتن Lazy Loading که باعث مصرف زیاد حافظه میشود.
- نداشتن intellisense حین فراخوانیها.
- نداشتن Navigation Property که باعث join نویسیهای زیاد خواهد شد.
- SQL در مقایسه با یک زبان مدرن ناقص بنظر میرسد و این نوشتن کد آن را سختتر میکند.
- نداشتن امکان تغییر منطقی نام جداول و ستون ها
- مدیریت تراکنشها بصورت دستی، حال آنکه با الگوی Unit Of Work این مشکل در یک ORM قدرتمند مثل EF حل شده است.
- زمان بر بودن نوشتن SP؛ گاهی نوشتن یک تابع در یک ORM یا بعضا نوشتن یک کوئری SQL کوتاه در یک رشته متنی، سادهتر از نوشتن کد SP است. آیا برای هر وظیفه کوچک در دیتابیس، نوشتن یک SP ضروری است؟
مزایای SP :
- کمتر کردن Round Trips در شبکه و متعاقبا کاهش ترافیک شبکه؛ اگر از یک فراخوانی استفاده کنیم، کاهش Round Tripها تاثیر چندانی نخواهد داشت. همچنین ارسال یک کوئری کامل، نسبت به ارسال فقط اسم SP و پارامترهای آن، پهنای باند بیشتری اِشغال میکند. البته در یک شبکه با سرعت قابل قبول، بعید است این دو مزیت محسوس باشند؛ اما به هر حال برای موارد خاص، دو مزیت محسوب میشوند. نکته دیگر آنکه بدلیل Pre-Compiled بودن SPها و همچنین کَش شدن Execution Plan آنها، اندکی با سرعت بالاتری اجرا میشوند.
- امکان چک کردن سینتکس قبل از اجرای آن؛ در مقایسه با Raw Query مزیت محسوب میشود.
- امکان به اشتراک گذاری کد؛ برای پروژههایی که چندین اپلیکیشن با چندین زبان برنامه نویسی مختلف در حال تهیه هستند و نیازمند دسترسی مستقیم به دادهها با سرعت به نسبت بالاتری هستند، SP میتواند یک راه حل ایده آل محسوب شود. بجای پیاده سازی منطق برنامه در هر اپلیکیشن بصورت جداگانه و زحمت کدنویسی هرکدام، میتوان از SP استفاده کرد. هرچند امروزه معمولا برای حل این مشکل، API های مشترک معماری Restful ارجحیت دارد.
- کمک به ایجاد یک پَک؛ در یک زیر سیستم با نیازمندی مشخص که اعمال تغییرات در آن محتمل نمیباشد نیز SP میتواند یک گزینه مناسب به حساب آید. مثلا یک سیستم Membership را در نظر بگیرید که در پروژههای مختلف شما مورد استفاده قرار خواهد گرفت. برای مثال میشود یک سیستم Membership سفارشی را با امکان Hash پسورد و رمز کردن دادههای حساس، به کمک SP و Function های مناسب فراهم کرد و در واقع بین Application Login و Data Logic تمایز قائل شد. شخصا معماری Restful را به این روش هم ترجیح میدهم.
- بهرمند شدن از امکانات بومی SQL ؛ به عنوان نمونه برای ترانهاده کردن خروجی یک کوئری میتوان از فانکشن Pivot استفاده کرد. یا فانکشنهای تحلیلی Lead و Lag (لینک مستندات اوراکل این دو فانکشن به ترتیب در ^ و ^ ) که بنظر نمیرسد هنوز معادل مستقیمی درORM ها داشته باشند.
- تسلط و کنترل بیشتر و دقیقتر بر کوئری نهایی؛ گفته میشود SP و عبارات SQL در دیتابیس، حکم assembly را در سایر زبانها دارند. بنابراین با SP میتوان عبارات SQL و نحوه اجرای آن را در دیتابیس، بطور کامل تحت فرمان داشت. این در حالی است که هر یک از ORMها دستورات زبان برنامه نویسی مبداء را به یک عبارت SQL ترجمه میکنند که این عبارت چندان تحت کنترل برنامه نویس نیست و بیشتر به مدل کاری ORM بستگی دارد.
- امکان join بین دو یا چند دیتابیس مجزا؛ حال آنکه امکان join بین دو Context در ORM ها وجود ندارد. بعلاوه اگر دو دیتابیس مدنظر ما روی دو سرور مجزا باشند، با SP و کانفیگ Linked Server کماکان میشود کوئری join دار نوشت.
- برای عملیاتهای Batch مناسبتر است؛ در مقام مقایسه با ORM ها که با تکنیکهای مختلفی سعی در افزایش سرعت عملیات Batch، بخصوص Insert و Update را دارند، SP با سرعت قابل قبولتری اجرا میشود.
- عدم نیاز به یادگیری سینتکس و ابزاری جدید؛ موارد
بسیاری وجود دارند که فرصت یادگیری تکنولوژی جدیدی مثل یک ORM و یا SQL Bulk و حتی کتابخانههای ثالث مبتنی بر این ابزارها وجود ندارند و ممکن است مجبور شوید برای باقی ماندن در بازار رقابتی، از
دانستههای قبلی خود استفاده کنید .
- تخصصیتر کردن وظایف؛ برنامه نویسهای دیتابیس به صورت تخصصی اقدام به تحلیل روابط و ایندکسها میکنند، دیتابیس را ایجاد و نرمال سازی مینمایند، SP های متناسب را میسازند و به بهترین شکل Optimize و در آخر تست میکنند.
- امنیت به نسبت بالاتر؛ میتوان مجوز اجرای SP را به یک کاربر اعطا کرد، بدون آنکه مجوز دسترسی به جداول مورد استفاده در آن SP را داد. همچنین نسبت به کوئریهای پارامتری نشده، SQL ارجیحت دارند چون احتمال آسیب پذیری در مقابل SQL Injection را کمتر میکنند.
نتیجهگیری
اگرچه SP ها برای پردازش دادهها آنقدر هم که در وبلاگها میخوانیم بد نیستند، اما سوء استفاده از آن، مشکلات عدیدهای را ایجاد خواهد کرد. با توجه به روند تغییرات تکنولوژیهای دسترسی به دادهها و معماریهای مدرن بنظر میرسد SP در بهترین حالت، ابزار مناسبی برای انجام عملیات CRUD است و نه بیشتر؛ مگر در مواردی خاص که به تشخیص شما نیاز به استفاده بیشتر از آن وجود داشته باشد.
کدام نگارش از ویندوز، از ICU پشتیبانی میکند؟
تمام ویندوزهای پس از Windows 10 May 2019 Update، به همراه icu.dll، به عنوان جزء استاندارد سیستم عامل هستند. بنابراین دات نت 5 و نگارشهای پس از آن، در این سیستم عاملها، از سرویس بومی سازی ICU استفاده خواهند کرد؛ اما اگر از نگارشهای پیشین ویندوز استفاده میکنید، به اجبار به سیستم NLS سوئیچ خواهد شد.
تاثیر ICU بر برنامههای دات نت 5 به بعد
قطعه کد زیر را درنظر بگیرید:
string s = "Hello\r\nworld!"; int idx = s.IndexOf("\n"); Console.WriteLine(idx);
حالت «پیشفرض» جستجو و مقایسهی رشتهها در دات نت 5 به بعد، یک مقایسهی مبتنی بر «دستورات زبانی» بر اساس فرهنگ تنظیم شدهی در Thread جاری برنامهاست (یا همان System.Threading.Thread.CurrentThread.CurrentCulture).
چرا متدهای کار بر روی رشتهها در دات نت 5 به بعد، نسبت به نگارشهای قبلی متفاوت عمل میکنند؟
زمانیکه متدی مانند IndexOf فراخوانی میشود، هدف عمدهی برنامهنویسها، یک جستجوی Ordinal است (یعنی مقایسهی کاراکتر به کاراکتر؛ بدون درنظر گرفتن نکات زبانی و بومی)؛ اما فراموش میکنند که این متدها دارای پارامتر دومی هم هستند که از نوع StringComparison است و سالها است که توصیه میشود این پارامتر را هم به صورت صریحی مقدار دهی کنید تا هدف خود را از نوع جستجو دقیقا مشخص نمائید. از زمان دات نت 5 به بعد، اگر این پارامتر را مشخص نکنید، جستجوی صورت گرفته یک رفتار culture-specific را خواهد داشت و نه Ordinal. از این لحاظ مقایسهی رشتهها توسط استانداردهای ICU و NLS، بر اساس پیاده سازیهای مختلف زبانشناسی، خروجیهای یکسانی را ارائه نمیدهند و به همین جهت است که اینبار خروجی منهای یک را دریافت میکنیم.
یک نکته: خروجی قطعه کد فوق در سیستمهای لینوکسی که از .NET Core 2x - 3x. هم استفاده میکنند، دقیقا منهای یک است؛ چون پیشفرض بومی سازی آنها نیز ICU است.
چگونه میتوان به همان حالت پیشین مقایسهی رشتهها در NET. بازگشت؟
مایکروسافت بستهی نیوگت Microsoft.CodeAnalysis.FxCopAnalyzers را جهت گوشزد کردن نکتهی ذکر صریح StringComparison، به روز رسانی کردهاست. بنابراین بهتر است تا آنرا به پروژهی خود اضافه کنید. در این حالت اخطارهای مناسبی را جهت یافتن قسمتهای مشکلدار برنامهی خود دریافت میکنید. برای مثال برای اینکه در قطعه کد فوق به همان پاسخ متداول 6 برسیم، تنها کافیاست پارامتر دوم StringComparison را ذکر کنیم:
int idx = s.IndexOf("\n", StringComparison.Ordinal);
و یا حتی میتوانید فایل csproj پروژهی خود را ویرایش کرده و یک سطر زیر را به آن اضافه کنید:
<ItemGroup> <RuntimeHostConfigurationOption Include="System.Globalization.UseNls" Value="true" /> </ItemGroup>
کدام متدهای کار با رشتهها در دات نت 5، تحت تاثیر این تغییرات قرار گرفتهاند؟
اگر از متدهای زیر در برنامههای خود استفاده میکنید، نکتهی ذکر پارامتر StringComparison.Ordinal را فراموش نکنید:
System.String.Compare System.String.EndsWith System.String.IndexOf System.String.StartsWith System.String.ToLower System.String.ToLowerInvariant System.String.ToUpper System.String.ToUpperInvariant System.Globalization.TextInfo (most members) System.Globalization.CompareInfo (most members) System.Array.Sort (when sorting arrays of strings) System.Collections.Generic.List<T>.Sort() (when the list elements are strings) System.Collections.Generic.SortedDictionary<TKey,TValue> (when the keys are strings) System.Collections.Generic.SortedList<TKey,TValue> (when the keys are strings) System.Collections.Generic.SortedSet<T> (when the set contains strings)
سؤال: اگر متدی پارامتر دوم StringComparison را نداشت چطور؟
اگر به ماخذ «Behavior changes when comparing strings on .NET 5» مراجعه کنید، در انتهای آن جدولی را ارائه داده که دو سطر اول آن، به صورت زیر است:
API Default behavior Remarks string.Compare CurrentCulture
برای مطالعهی بیشتر:
Behavior changes when comparing strings on .NET 5+
.NET globalization and ICU.
Globalization breaking changes
بحث و گفتگویی در این مورد