در نگارش 2
public virtual DateTime? LockoutEndDateUtc { get; set; }
public virtual DateTimeOffset? LockoutEnd { get; set; }
مشکل ساختار DateTime چیست؟
تمام کسانیکه مدتی با NET Framework. کار کردهاند، قطعا از ساختار DateTime برای ذخیره سازی اطلاعاتی زمانی محلی استفاده کردهاند. اما مشکل DateTime چیست؟
فرض کنید در حال استفادهی از یک وب سرویس قرار گرفتهی در یک منطقهی زمانی غربی هستید و این وب سرویس تاریخ تولد افراد را با یک چنین فرمتی ارائه میدهد:
2012-03-01 00:00:00-05:00
var dateString = "2012-03-01 00:00:00-05:00"; var birthDay = DateTime.Parse(dateString);
2012-02-29 11:00:00 PM
چگونه میتوان offset را در تاریخ ذکر کرد، اما از تبدیل آن به زمان محلی جلوگیری کرد؟ این مورد جاییاست که ساختار DateTimeOffset بکار خواهد آمد.
DateTimeOffset و ذخیرهی DateTime به همراه Offset
ساختار کلی DateTimeOffset بسیار واضح بوده و تشکیل شدهاست از Date + Time + Offset. اهمیت آن نیز به ذخیره سازی اطلاعات منطقهی زمانی، در قسمت Offset ساختار ارائه شده بر میگردد. ساختار DateTimeOffset در بسیاری از موارد با DateTime متداول یکسان است و تفاوتهای آن شامل خواص اضافی ذیل هستند:
- DateTime: قسمت DateTime مقدار را بدون توجه به offset باز میگرداند (به زمان محلی تبدیل نخواهد شد).
- LocalDateTime: قسمت DateTime را با توجه به منطقه زمانی سروری که برنامه بر روی آن اجرا میشود، بر میگرداند.
- Offset: فاصلهی زمانی با UTC را بیان میکند. یک TimeSpan است که فاصلهی با UTC را بیان میکند.
- UtcDateTime: قسمت DateTime را با توجه به UTC time ارائه میکند.
در این ساختار خواص Now و UtcNow نیز یک DateTimeOffset را باز میگردانند.
چه زمانی از DateTime و چه زمانی از DateTimeOffset استفاده کنیم؟
اگر هدف شما ذخیره سازی اطلاعات زمانی محلی (جایی که سرور برنامه قرار دارد) است، از DateTime استفاده کنید. اما اگر میخواهید مقادیر زمانی را در مناطق زمانی دیگری نیز مورد استفاده قرار دهید و علاقمندید که قسمت TimeZone این اطلاعات نیز حفظ شود، از DateTimeOffset استفاده نمائید.
در این حالت روش پردازش صحیح مثال ابتدای بحث به صورت ذیل خواهد بود:
string birthDay = "2012-03-01 00:00:00-05:00"; var dtOffset = DateTimeOffset.Parse(birthDay);
var theDay = dtOffset.Date;
SQL Server و پشتیبانی از DateTimeOffset
ساختار دادهای datetime در SQL Server نیز اطلاعات منطقهی زمانی را ذخیره نمیکند و درصورت بازیابی آن در برنامه، این زمان، به زمان محلی تبدیل خواهد شد. برای رفع این مشکل، از زمان ارائهی SQL Server 2008، ساختار DateTimeOffset نیز به نوعهای دادهآی SQL Server اضافه شدهاست:
این ساختار، اطلاعات +00:00 timezone را نیز ذخیره میکند.
مشکلات نوع datetime در بانکهای اطلاعاتی برای ذخیره سازی اطلاعات UTC در آنها
یکی از روشهای توصیه شدهی جهت ذخیره سازی اطلاعات زمانی در بانکهای اطلاعاتی، استفادهی از DateTime.UtcNow است. اما زمانیکه از DateTime.UtcNow برای ذخیره سازی اطلاعاتی زمانی استفاده میکنیم، به معنای دریافت زمان محلی بر اساس و نسبت به UTC است. در این حالت هنگامیکه آنرا از یک فیلد datetime بانک اطلاعاتی بازیابی میکنیم، از نوع Unspecified خواهد بود (DateTimeKind.Unspecified) و به صورت خودکار به DateTimeKind.Local ترجمه میشود. یعنی مقدار آن مجددا به زمان محلی شیفت پیدا خواهد کرد چون نوع datetime بانک اطلاعاتی درکی از DateTimeKind و منطقهی زمانی ندارد.
به همین جهت روش بازیابی صحیح این زمان UTC، نیاز به قید صریح DateTimeKind.Utc را خواهد داشت:
public static class SqlDataReaderExtensions { public static DateTime GetDateTimeUtc(this SqlDataReader reader, string name) { int fieldOrdinal = reader.GetOrdinal(name); DateTime unspecified = reader.GetDateTime(fieldOrdinal); return DateTime.SpecifyKind(unspecified, DateTimeKind.Utc); } }
خلاصهی بحث
اگر برنامهی وب شما امروز در یک سرور در اروپا هاست میشود و سال بعد در یک سرور کانادایی، استفادهی DateTime.UtcNow کمک زیادی به برنامه نکرده و خروجی SQL Server در این حالت DateTimeKind.Unspecified است و این زمان مجددا بر اساس محل سرور جدید و تنظیمات منطقهی زمانی آن، به حالت DateTimeKind.Local شیفت داده میشود که الزاما خروجی صحیحی را به همراه نخواهد داشت و یا اگر قرار است از وب سرویس شما در مناطق زمانی مختلفی استفاده کنند نیز DateTime.UtcNow انتخاب مناسبی نیست. جهت درج فاصلهی صحیح با UTC و ذخیره سازی آن در بانک اطلاعاتی، روش توصیه شده، استفاده از نوع DateTimeOffset است و در این حالت دیگر SQL Server اطلاعات را با فرمت زمانی Unspecified بازگشت نمیدهد و در سمت کلاینت نیازی به تبدیلات خاصی نخواهد بود.
استفاده یا عدم استفاده از یک تکنولوژی یا ابزار خاص، به پارامترهای مختلفی از جمله ابعاد پروژه، مهارت و دانش اعضای تیم، ماهیت پروژه، پلتفرم اجرا، بودجهی پروژه، مهلت تکمیل پروژه و تعداد نفرات تیم بستگی دارد. بنابراین واضح است پیچیدن یک نسخهی خاص، برای همهی سناریوها امکان پذیر نیست؛ اما شرایطی وجود دارد که استفاده یا عدم استفاده از این ابزارهای تکنولوژیک منطقیتر مینمایند.
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 است و نه بیشتر؛ مگر در مواردی خاص که به تشخیص شما نیاز به استفاده بیشتر از آن وجود داشته باشد.
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;
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;
- چه تعداد سطح برای پشتیبانی در درخت نیاز خواهیم داشت؟
- من همیشه از کار با کدی که ساختار دادهی درختی را مدیریت میکند، میترسم
- من باید اسکریپتی را به طور دورهای اجرا نمایم تا سطرهای یتیم موجود در درخت را حذف کند.
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;
SELECT * FROM Comments START WITH comment_id = 9876 CONNECT BY PRIOR parent_id = comment_id;
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)
SELECT * FROM Comments AS c WHERE '1/4/6/7/' LIKE c.path || '%' ;
SELECT * FROM Comments AS c WHERE c.path LIKE '1/4/' || '%' ;
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;
SELECT c2.* FROM Comments AS c1 JOIN Comment AS c2 ON c1.nsleft BETWEEN c2.nsleft AND c2.nsright WHERE c1.comment_id = 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;
-- 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!' );
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;
SELECT c.* FROM Comments AS c JOIN TreePaths AS t ON c.comment_id = t.ancestor WHERE t.descendant = 6;
INSERT INTO TreePaths (ancestor, descendant) SELECT t.ancestor, 8 FROM TreePaths AS t WHERE t.descendant = 5 UNION ALL SELECT 8, 8;
DELETE FROM TreePaths WHERE descendant = 7;
DELETE FROM TreePaths WHERE descendant IN (SELECT descendant FROM TreePaths WHERE ancestor = 4);
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);
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 را برای ایجاد پرسوجو روی فرزندان و پدر بلافصل را آسانتر نیز نمود. اگر فیلد path_length را به جدول TreePaths اضافه نماییم این کار انجام میشود. path_length گرهای که به خودش ارجاع میشود، صفر است. path_length فرزند بلافصل هر گره 1، path_length نوهی آن 2 میباشد و به همین ترتیب path_lengthها را در هر سطر مقداردهی میکنیم. اکنون یا فتن فرزند گرهی شمارهی 4 آسانتر است:
SELECT * FROM TreePaths WHERE ancestor = 4 AND path_length = 1;
ولی در کل محیط راحت و سبکی هست و امکانات زیادی داره و نیاز نیست با vs سنگین سر و کله زد. به خصوص اگر قبلا با ویراستارهای شرکت جت برینز کار کرده باشید محیط فوق العاده ای میشه
- Lead Function:
LEAD ( scalar_expression [ ,offset ] , [ default ] ) OVER ( [ partition_by_clause ] order_by_clause )
- Scalar_expression: در Scalar_expression، نام یک فیلد یا ستون درج میشود، و مقدار برگشتی فیلد مورد نظر، به مقدار تعیین شده offset نیز بستگی دارد. خروجی Scalar_expression فقط یک مقدار است.
- offset: منظور از Offset در این Syntax همانند عملکرد Offset در Syntax مربوط به Over میباشد. یعنی هر عددی برای offset در نظر گرفته شود، بیانگر نقطه آغازین سطر بعدی یا قبلی نسبت به سطر جاری است. به بیان دیگر، عدد تعیین شده در Offset به Sql server میفهماند چه تعداد سطر را در محاسبه در نظر نگیرد.
- Default: زمانی که برای Offset مقداری را تعیین مینمایید، SQL Server به تعداد تعیین شده در Offset، سطرها را در نظر نمیگیرد، بنابراین مقدار خروجی Scalar_expression بطور پیش فرض Null در نظر گرفته میشود، چنانچه بخواهید، مقداری غیر از Null درج نمایید، میتوانید مقدار دلخواه را در قسمت Default وارد کنید.
- (OVER ( [ partition_by_clause ] order_by_clause : در بخش اول بطور کامل توضیح داده شده است.
Create Table TestLead_LAG (SalesOrderID int not null, SalesOrderDetailID int not null , OrderQty smallint not null); GO Insert Into TestLead_LAG Values (43662,49,1),(43662,50,3),(43662,51,1), (43663,52,1),(43664,53,1),(43664,54,1), (43667,77,3),(43667,78,1),(43667,79,1), (43667,80,1),(43668,81,3),(43669,110,1), (43670,111,1),(43670,112,2),(43670,113,2), (43670,114,1),(43671,115,1),(43671,116,2)
مثال:قصد داریم در هر سطر مقدار بعدی فیلد SalesOrderDetailID در فیلد دیگری به نام LeadValue نمایش دهیم، بنابراین Script زیر را ایجاد میکنیم:SELECT s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty, LEAD(SalesOrderDetailID) OVER (ORDER BY SalesOrderDetailID) LeadValue FROM TestLead_LAG s WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ORDER BY s.SalesOrderID,s.SalesOrderDetailID,s.OrderQtyخروجی بصورت زیر خواهد بود:
مطابق شکل، براحتی واضح است، که در هر سطر مقدار بعدی فیلد SalesOrderDetailID در فیلد LeadValue درج و نمایش داده میشود. فقط در سطر 10، چون مقدار بعدی برای فیلد SalesOrderDetailID وجود ندارد، SQL Server مقدار فیلد LeadValue را، Null در نظر میگیرد.در این مثال فقط از آرگومان Scalar_expression، استفاده کردیم، و Offset و Default را مقدار دهی ننمودیم، بنابراین SQL Server بطور پیش فرض هیچ سطری را حذف نمیکند و مقدار Default را Null در نظر میگیرد.مثال دوم: قصد داریم در هر سطر مقدار دو سطر بعدی فیلد SalesOrderDetailID را در فیلد LeadValue نمایش دهیم، و در صورت وجود نداشتن مقدار فیلد SalesOrderDetailID، مقدار پیش فرض صفر ،در فیلد LeadValue قرار دهیم،بنابراین Script آن بصورت زیر خواهد شد:SELECT s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty, LEAD(SalesOrderDetailID,2,0) OVER (ORDER BY SalesOrderDetailID) LeadValue FROM TestLead_LAG s WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ORDER BY s.SalesOrderID,s.SalesOrderDetailID,s.OrderQtyخروجی:
در صورت مسئله بیان کرده بودیم، در هر سطر،مقدار فیلد SalesOrderDetailID دو سطر بعدی، را نمایش دهیم، بنابراین مقداری که برای Offset در نظر میگیریم، برابر دو خواهد بود، سپس گفته بودیم، چنانچه در هر سطر مقدار فیلد SalesOrderDetailID وجود نداشت،بجای مقدار پیش فرض Null،از مقدار صفر استفاده شود، بنابراین به Default مقدار صفر را نسبت دادیم.LEAD(SalesOrderDetailID,2,0)در شکل، مطابق صورت مسئله، مقدار فیلد LeadValue سطر اول برابر است با 78،به بیان سادهتر برای بدست آوردن مقدار فیلد LaedValue هر سطر، میبایست هر سطر را به علاوه 2 (Offset) نماییم، تا سطر بعدی بدست آید، سپس مقدار SalesOrderDetailID را در فیلد LeadValue قرار میدهیم.به سطر 9 و 10 توجه نمایید، که مقدار فیلد LeadValue آنها برابر با صفر است، واضح است، سطر 10 + 2 برابر است با 12( 10+2=12 )، چنین سطری در خروجی نداریم، بنابراین بطور پیش فرض مقدار LeadVaule توسط Sql Server برابر Null در نظر گرفته میشود، اما نمیخواستیم، که این مقدار Null باشد، بنابراین به آرگومان Default مقدار صفر را نسبت دادیم، تا SQL Server ، به جای استفاده از Null، مقدار در نظر گرفته شده صفر را استفاده نماید.اگر چنین فانکشنی وجود نداشت، برای شبیه سازی آن میبایست از Join روی خود جدول استفاده مینمودیم، و یکسری محاسابت دیگر، که کار را سخت مینمود، مثال دوم را با Script زیر میتوان شبیه سازی نمود:WITH cteLead AS ( SELECT SalesOrderID,SalesOrderDetailID,OrderQty, ROW_NUMBER() OVER (ORDER BY SalesOrderDetailID) AS sn FROM TestLead_LAG WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ) SELECT m.SalesOrderID, m.SalesOrderDetailID, m.OrderQty, case when sLead.SalesOrderDetailID is null Then 0 Else sLead.SalesOrderDetailID END as leadvalue FROM cteLead AS m LEFT OUTER JOIN cteLead AS sLead ON sLead.sn = m.sn+2 ORDER BY m.SalesOrderID, m.SalesOrderDetailID, m.OrderQtyجدول موقتی ایجاد نمودیم، که ROW_Number را در آن اضافه کردیم، سپس جدول ایجاد شده را با خود Join کردیم، و گفتیم، که مقدار فیلدLeadValue هر سطر برابر است با مقدار فیلد SalesOrderDetailID دو سطر بعد از آن. و با Case نیز مقدار پیش فرض را صفر در نظر گرفتیم.
- LAG Function:
این فانکشن نیز در SQL Server 2012 ارائه شده است، و امکان دسترسی، به Dataهای سطر قبلی نسبت به سطر جاری را در نتیجه یک پرس و جو (Query)، ارائه میدهد. بدون آنکه از Self-join استفاده نمایید،Syntax آن شبیه به فانکشن Lead میباشد و بصورت زیر است:LAG (scalar_expression [,offset] [,default]) OVER ( [ partition_by_clause ] order_by_clause )Syntax مربوط به فانکشن LAG را شرح نمیدهم، بدلیل آنکه شبیه به فانکشن Lead میباشد، فقط تفاوت آن در Offset است، Offset در فانکشن LAG روی سطرهای ماقبل سطر جاری اعمال میگردد.مثال دوم را برای حالت LAG Function شبیه سازی مینماییم:SELECT s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty, LAG(SalesOrderDetailID,2,0) OVER (ORDER BY SalesOrderDetailID) LAGValue FROM TestLead_LAG s WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ORDER BY s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty goخروجی :
همانطور که گفتیم، LAG Function عکس LEAD Function میباشد. یعنی مقدار فیلد LAGValue سطر جاری برابر است با مقدار SalesOrderDetailID دو سطر ما قبل خود.مقدار فیلد LAGValue دو سطر اول و دوم نیز برابر صفر است، چون دو سطر ماقبل آنها وجود ندارد، و مقدار صفر نیز بدلیل این است که Default را برابر صفر در نظر گرفته بودیم.مثال: در این مثال از Laed Function و LAG Function بطور همزمان استفاده میکنیم، با این تفاوت، که از گروه بندی نیز استفاده شده است:Script زیر را اجرا نمایید:SELECT s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty, Lead(SalesOrderDetailID) OVER (PARTITION BY SalesOrderID ORDER BY SalesOrderDetailID) LeadValue, LAG(SalesOrderDetailID) OVER (PARTITION BY SalesOrderID ORDER BY SalesOrderDetailID) LAGValue FROM TestLead_LAG s WHERE SalesOrderID IN (43670, 43669, 43667, 43663) ORDER BY s.SalesOrderID,s.SalesOrderDetailID,s.OrderQty goخروجی:با بررسی هایی که در مثالهای قبل نمودیم،خروجی زیر را میتوان براحتی تشخیص داد، و توضیح بیشتری نمیدهم.موفق باشید.
در این قسمت میخواهیم بیشتر در خصوص توابع مرتبط با ساختار سلسله مراتبی صحبت کنیم. برای آشنایی با این توابع و امکانات MDX Query، مقاله را با بررسی چندین Query دنبال خواهیم کرد.
بدست آوردن تمامی برادران یک سطح خاص :
Select [Measures].[Internet Sales Amount] on columns, [Customer].[Customer Geography].[Customer].[Crystal Zheng].parent.children on rows From [Adventure Works]
در کوئری بالا تمامی مشتریانی را که دارای کد پستی مشابه با کد پستی [Crystal Zheng]. میباشند، واکشی کرده ایم.
به عبارت دیگر با اعمال [Crystal Zheng].parent، به کد پستی مشتری دسترسی پیدا کرده ایم (برای درک بیشتر در زیر ساختار سلسله مراتبی موقعیت جغرافیایی مشتریان را ببینید) و سپس با اعمال .children به تمامی مشتریان موجود در آن کد پستی رسیده ایم؛ که عملا همان برادران [Crystal Zheng] می باشند.
نتیجه کوئری بالا در زیر نمایش داده شده است
راه بهتر برای بدست آوردن تمامی برادران یک سطح، استفاده از تابع siblings میباشد.
Select [Measures].[Internet Sales Amount] on columns, [Customer].[Customer Geography].[Customer].[Crystal Zheng].siblings on rows From [Adventure Works]
کوئریهای بالا جواب یکسانی را بر میگردانند. به عبارت دیگر تابع siblings عملا کار دو تابع parent.children را انجام میدهد
برای بدست آوردن برادر ارشد به صورت زیر عمل میکنیم (اولین بچه در ساختار سلسله مراتبی)
Select [Measures].[Internet Sales Amount] on columns, [Customer].[Customer Geography].[Customer].[Crystal Zheng].parent.firstchild on rows From [Adventure Works]
و یا از تابع زیر استفاده میکنیم
Select [Measures].[Internet Sales Amount] on columns, [Customer].[Customer Geography].[Customer].[Crystal Zheng].firstsibling on rows From [Adventure Works]
هر دو کوئری به جواب یکسان خواهند رسید.
و برای بدست آوردن آخرین برادر در ساختار سلسله مراتبی (برادر ته تغاری) از دو روش زیر میتوان استفاده کرد.
Select [Measures].[Internet Sales Amount] on columns, [Customer].[Customer Geography].[Customer].[Crystal Zheng].parent.lastchild on rows From [Adventure Works]
یا
Select [Measures].[Internet Sales Amount] on columns, [Customer].[Customer Geography].[Customer].[Crystal Zheng].lastsibling on rows From [Adventure Works]
برای توضیح بیشتر میتوان اضافه کرد که در کوئری بالا میزان فروش اینترنتی را برای آخرین مشتری در موقعیت جغرافیایی مشتری با نام [Crystal Zheng] واکشی شده است.
حال تصور کنید بخواهیم میزان فروش اینترنتی را برای تمامی مشتریان ایالت [Yveline] بدست بیاوریم. در این صورت MDX Query به شکل زیر خواهد بود
Select [Measures].[Internet Sales Amount] on columns, descendants( [Customer].[Customer Geography].[State-Province].[Yveline] ,[Customer].[Customer Geography].[Customer] )on rows From [Adventure Works]
تابع descendants دارای دو پارامتر میباشد. اولی برای مشخص نمودن شروع و مبدا در ساختار سلسله مراتبی و دومین برای مشخص کردن سطح واکشی در ساختار سلسله مراتبی میباشد. به عبارت دیگر در کوئری بالا تمامی زاد و رود ایالت [Yveline] در سطح شهر واکشی شده است و میزان فروش اینترنتی آن نمایش داده شده است.
در زیر یک کوئری ترکیبی با استفاده از دو تابع ancestor و descendants نوشته شده است.
Select [Measures].[Internet Sales Amount] on columns, descendants( ancestor( [Customer].[Customer Geography].[Customer].[Crystal Zheng], [Customer].[Customer Geography].[State-Province] ) ,[Customer].[Customer Geography].[Customer] )on rows From [Adventure Works]
در اینجا ابتدا جد یک مشتری در سطح ایالت بدست آمده سپس زاد و رود آن در سطح مشتری بدست می آید .
برای بدست آوردن فروش اینترنتی تمامی شهرهای کشور فرانسه میتوانیم به صورت زیر عمل کنیم.
Select [Measures].[Internet Sales Amount] on columns, descendants( [Customer].[Customer Geography].[Country].[France], [Customer].[Customer Geography].[City] ) on rows From [Adventure Works]
تابع descendants دارای یک پارامتر سوم هم میباشد که مشخص کنندهی میزان واکشی سطوح میباشد و به صورت پیش فرض Self میباشد. بنابر این کوئری بالا و پایین ، نتیجه یکسان خواهند داشت
Select [Measures].[Internet Sales Amount] on columns, descendants( [Customer].[Customer Geography].[Country].[France], [Customer].[Customer Geography].[City], self )on rows From [Adventure Works]
حال اگر بخواهیم فروش اینترنتی را برای تمامی زاد و رود کشور فرانسه از سطح شهر به پایین واکشی کنیم داریم :
Select [Measures].[Internet Sales Amount] on columns, descendants( [Customer].[Customer Geography].[Country].[France], [Customer].[Customer Geography].[City], self_and_after ) on rows From [Adventure Works]
در این حالات تمامی زاد و رود کشور فرانسه از سطح شهر به پایین در خروجی قرار می گیرد .به این صورت که ابتدا اولین شهر می آید؛ سپس اولین کد پستی در آن شهر و بعد تمامی مشتری های آن کد پستی و بعد کد پستی بعدی و ...
به دست آوردن تمامی زاد و رود فرانسه از سطح بعد از شهر .
به عبارت دیگر ، خروجی باتوجه به ساختار سلسله مراتبی تعریف شده عبارت است از کد پستی و تمام مشتریان آن کد پستی و سپس کد پستی بعدی .
Select [Measures].[Internet Sales Amount] on columns, descendants( [Customer].[Customer Geography].[Country].[France], [Customer].[Customer Geography].[City], after )on rows From [Adventure Works]
در کوئری فوق، خود شهر در خروجی نمایش داده نمیشود.
به دست آوردن زاد و رود فرانسه تا یک سطح قبل از شهر .
در این حالت فرانسه و تمامی ایالت های آن در خروجی آورده می شود .
Select [Measures].[Internet Sales Amount] on columns, descendants( [Customer].[Customer Geography].[Country].[France], [Customer].[Customer Geography].[City], before )on rows From [Adventure Works]
همچنین میتوان دومین پارامتر تابع را به صورت عدد وارد کرد و این عدد بیانگر تعداد سطح پایینتر از پارامتر اول در ساختار سلسله مراتبی میباشد.
به عنوان مثال :
Select [Measures].[Internet Sales Amount] on columns, descendants( [Customer].[Customer Geography].[Country].[France], 2, before ) on rows From [Adventure Works]
در این حالت فرانسه و تمامی ایالت های آن در خروجی قرار می گیرد .
در ابتدا دو سطح ار کشور پایین می رویم و به شهر می رسیم و بعد زاد و رود فرانسه تا یکی قبل از شهر را بر می گرداند .
در قسمتهای بعدی در خصوص دیگر توابع مرتبط با ساختارهای سلسله مراتبی، توضیحاتی را ارایه خواهم کرد.
تبدیل کوئری های sql به LINQ
string greeting = "Hello, C#";
که در این حالت مجموعهای از کاراکترها را ایجاد خواهد کرد:
- خانههای آن یک ضرب پر نمیشوند بلکه به ترتیب، خانه به خانه پر میشوند.
- قبل از انتساب متن باید باید از طول متن مطمئن شویم تا بتوانیم تعداد خانهها را بر اساس آن ایجاد کنیم.
- همه عملیات آرایهها از پر کردن ابتدای کار گرفته تا هر عملی، نیاز است به صورت دستی صورت بگیرد و تعداد خطوط کد برای هر کاری هم بالا میرود.
string str = "abcde"; char ch = str[1]; // ch == 'b' str[1] = 'a'; // Compilation error! ch = str[50]; // IndexOutOfRangeException
string a="Hello \"C#\""; string b="Hello \r\n C#"; //مساوی با اینتر string c="C:\\a.jpg"; //چاپ خود علامت \ -مسیردهی
string c=@"C:\a.jpg";// == "C:\\a.jpg"
string source = "Some source"; string assigned = source;
string hel = "Hel"; string hello = "Hello"; string copy = hel + "lo";
string hello = "Hello"; string same = "Hello";
برای اطلاعات بیشتر در این زمینه این لینک را مطالعه نمایید.
Console.WriteLine(word1.Equals(word2, StringComparison.CurrentCultureIgnoreCase));
string score = "sCore"; string scary = "scary"; Console.WriteLine(score.CompareTo(scary)); Console.WriteLine(scary.CompareTo(score)); Console.WriteLine(scary.CompareTo(scary)); // Console output: // 1 // -1 // 0
string alpha = "alpha"; string score1 = "sCorE"; string score2 = "score"; Console.WriteLine(string.Compare(alpha, score1, false)); Console.WriteLine(string.Compare(score1, score2, false)); Console.WriteLine(string.Compare(score1, score2, true)); Console.WriteLine(string.Compare(score1, score2, StringComparison.CurrentCultureIgnoreCase)); // Console output: // -1 // 1 // 0 // 0
نکته : برای مقایسه برابری دو رشته از متد Equals یا == استفاده کنید و فقط برای تعیین کوچک یا بزرگ بودن از compareها استفاده نمایید. دلیل آن هم این است که برای مقایسه از فرهنگ culture فعلی سیستم استفاده میشود و نظم جدول یونیکد را رعایت نمیکنند و ممکن است بعضی رشتههای نابرابر با یکدیگر برابر باشند. برای مثال در زبان آلمانی دو رشته "SS" و "ß " با یکدیگر برابر هستند.
عبارات با قاعده Regular Expression
string doc = "Smith's number: 0898880022\nFranky can be " + "found at 0888445566.\nSteven's mobile number: 0887654321"; string replacedDoc = Regex.Replace( doc, "(08)[0-9]{8}", "$1********"); Console.WriteLine(replacedDoc); // Console output: // Smith's number: 08******** // Franky can be found at 08********. // Steven' mobile number: 08********
اتصال رشتهها در Loop
DateTime dt = DateTime.Now; string s = ""; for (int index = 1; index <= 20000; index++) { s += index.ToString(); } Console.WriteLine(s); Console.WriteLine(dt); Console.WriteLine(DateTime.Now); Console.ReadKey();
- قسمتی از حافظه به طور موقت برای این دور جدید حلقه، گرفته میشود که به آن بافر میگوییم.
- رشته قبلی به بافر انتقال میابد که بسته به مقدار آن زمان بر و کند است؛ 5 کیلو یا 5 مگابایت یا 50 مگابایت و ...
- شماره تولید شده جدید به بافر چسبانده میشود.
- بافر به یک رشته تبدیل میشود وجایی برای خود در حافظه Heap میگیرد.
- حافظه رشته قدیمی و بافر دیگر بلا استفاده شدهاند و توسط GC پاکسازی میشوند که ممکن است عملیاتی زمان بر باشد.
String Builder
string declared = "Intern pool"; string built = new StringBuilder("Intern pool").ToString();
StringBuilder sb = new StringBuilder(); sb.Append("Numbers: "); DateTime dt = DateTime.Now; for (int index = 1; index <= 200000; index++) { sb.Append(index); } Console.WriteLine(sb.ToString()); Console.WriteLine(dt); Console.WriteLine(DateTime.Now); Console.ReadKey();
StringBuilder sb = new StringBuilder(15); sb.Append("Hello, C#!");
استفاده از متد ایستای string.Format
DateTime date = DateTime.Now; string name = "David Scott"; string task = "Introduction to C# book"; string location = "his office"; string formattedText = String.Format( "Today is {0:MM/dd/yyyy} and {1} is working on {2} in {3}.", date, name, task, location); Console.WriteLine(formattedText);
var sql = ctx.Artists.Where(a => a.Name == "name 1").ToQueryString();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.LogTo(Console.WriteLine);
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) => optionsBuilder.LogTo(message => Console.WriteLine(message));