استفاده از StructureMap به عنوان یک IoC Container
استفاده یا عدم استفاده از یک تکنولوژی یا ابزار خاص، به پارامترهای مختلفی از جمله ابعاد پروژه، مهارت و دانش اعضای تیم، ماهیت پروژه، پلتفرم اجرا، بودجهی پروژه، مهلت تکمیل پروژه و تعداد نفرات تیم بستگی دارد. بنابراین واضح است پیچیدن یک نسخهی خاص، برای همهی سناریوها امکان پذیر نیست؛ اما شرایطی وجود دارد که استفاده یا عدم استفاده از این ابزارهای تکنولوژیک منطقیتر مینمایند.
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 است و نه بیشتر؛ مگر در مواردی خاص که به تشخیص شما نیاز به استفاده بیشتر از آن وجود داشته باشد.
روشهای زیادی برای پیاده سازی این نوع جستجوها وجود دارد. در این مقاله سعی شده گامهای ایجاد یک ساختار پایه برای این نوع فرمها و یک ایجاد فرم نمونه بر پایه ساختار ایجاد شده را با استفاده از یکی از همین روشها شرح دهیم.
اساس این روش تولید عبارت Linq بصورت پویا با توجه به انتخابهای کاربرمی باشد.
1- برای شروع یک سلوشن خالی با نام DynamicSearch ایجاد میکنیم. سپس ساختار این سلوشن را بصورت زیر شکل میدهیم.
در این مثال پیاده سازی در قالب ساختار MVVM در نظر گرفته شده. ولی محدودتی از این نظر برای این روش قائل نیستیم.
2- کار را از پروژه مدل آغاز میکنیم. جایی که ما برای سادگی کار، 3 کلاس بسیار ساده را به ترتیب زیر ایجاد میکنیم:
namespace DynamicSearch.Model { public class Person { public Person(string name, string family, string fatherName) { Name = name; Family = family; FatherName = fatherName; } public string Name { get; set; } public string Family { get; set; } public string FatherName { get; set; } } } using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DynamicSearch.Model { public class Teacher : Person { public Teacher(int id, string name, string family, string fatherName) : base(name, family, fatherName) { ID = id; } public int ID { get; set; } public override string ToString() { return string.Format("Name: {0}, Family: {1}", Name, Family); } } } namespace DynamicSearch.Model { public class Student : Person { public Student(int stdId, Teacher teacher, string name, string family, string fatherName) : base(name, family, fatherName) { StdID = stdId; Teacher = teacher; } public int StdID { get; set; } public Teacher Teacher { get; set; } } }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using DynamicSearch.Model; namespace DynamicSearch.Service { public class StudentService { public IList<Student> GetStudents() { return new List<Student> { new Student(1,new Teacher(1,"Ali","Rajabi","Reza"),"Mohammad","Hoeyni","Sadegh"), new Student(2,new Teacher(2,"Hasan","Noori","Mohsen"),"Omid","Razavi","Ahmad"), }; } } }
جهت ساخت عبارت، نیاز به سه نوع جزء داریم:
-اتصال دهنده عبارات ( "و" ، "یا")
-عملوند (در اینجا فیلدی که قصد مقایسه با عبارت مورد جستجوی کاربر را داریم)
-عملگر ("<" ، ">" ، "=" ، ....)
برای ذخیره المانهای انتخاب شده توسط کاربر، سه کلاس زیر را ایجاد میکنیم (همان سه جزء بالا):
using System; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public class AndOr { public AndOr(string name, string title,Func<Expression,Expression,Expression> func) { Title = title; Func = func; Name = name; } public string Title { get; set; } public Func<Expression, Expression, Expression> Func { get; set; } public string Name { get; set; } } } using System; namespace DynamicSearch.ViewModel.Base { public class Feild : IEquatable<Feild> { public Feild(string title, Type type, string name) { Title = title; Type = type; Name = name; } public Type Type { get; set; } public string Name { get; set; } public string Title { get; set; } public bool Equals(Feild other) { return other.Title == Title; } } } using System; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public class Operator { public enum TypesToApply { String, Numeric, Both } public Operator(string title, Func<Expression, Expression, Expression> func, TypesToApply typeToApply) { Title = title; Func = func; TypeToApply = typeToApply; } public string Title { get; set; } public Func<Expression, Expression, Expression> Func { get; set; } public TypesToApply TypeToApply { get; set; } } }
using System.Collections.ObjectModel; using System.Linq; using System.Linq.Expressions; namespace DynamicSearch.ViewModel.Base { public abstract class SearchFilterBase<T> : BaseViewModel { protected SearchFilterBase() { var containOp = new Operator("شامل باشد", (expression, expression1) => Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1), Operator.TypesToApply.String); var notContainOp = new Operator("شامل نباشد", (expression, expression1) => { var contain = Expression.Call(expression, typeof(string).GetMethod("Contains"), expression1); return Expression.Not(contain); }, Operator.TypesToApply.String); var equalOp = new Operator("=", Expression.Equal, Operator.TypesToApply.Both); var notEqualOp = new Operator("<>", Expression.NotEqual, Operator.TypesToApply.Both); var lessThanOp = new Operator("<", Expression.LessThan, Operator.TypesToApply.Numeric); var greaterThanOp = new Operator(">", Expression.GreaterThan, Operator.TypesToApply.Numeric); var lessThanOrEqual = new Operator("<=", Expression.LessThanOrEqual, Operator.TypesToApply.Numeric); var greaterThanOrEqual = new Operator(">=", Expression.GreaterThanOrEqual, Operator.TypesToApply.Numeric); Operators = new ObservableCollection<Operator> { equalOp, notEqualOp, containOp, notContainOp, lessThanOp, greaterThanOp, lessThanOrEqual, greaterThanOrEqual, }; SelectedAndOr = AndOrs.FirstOrDefault(a => a.Name == "Suppress"); SelectedFeild = Feilds.FirstOrDefault(); SelectedOperator = Operators.FirstOrDefault(a => a.Title == "="); } public abstract IQueryable<T> GetQuarable(); public virtual ObservableCollection<AndOr> AndOrs { get { return new ObservableCollection<AndOr> { new AndOr("And","و", Expression.AndAlso), new AndOr("Or","یا",Expression.OrElse), new AndOr("Suppress","نادیده",(expression, expression1) => expression), }; } } public virtual ObservableCollection<Operator> Operators { get { return _operators; } set { _operators = value; NotifyPropertyChanged("Operators"); } } public abstract ObservableCollection<Feild> Feilds { get; } public bool IsOtherFilters { get { return _isOtherFilters; } set { _isOtherFilters = value; } } public string SearchValue { get { return _searchValue; } set { _searchValue = value; NotifyPropertyChanged("SearchValue"); } } public AndOr SelectedAndOr { get { return _selectedAndOr; } set { _selectedAndOr = value; NotifyPropertyChanged("SelectedAndOr"); NotifyPropertyChanged("SelectedFeildHasSetted"); } } public Operator SelectedOperator { get { return _selectedOperator; } set { _selectedOperator = value; NotifyPropertyChanged("SelectedOperator"); } } public Feild SelectedFeild { get { return _selectedFeild; } set { Operators = value.Type == typeof(string) ? new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.String)) : new ObservableCollection<Operator>(Operators.Where(a => a.TypeToApply == Operator.TypesToApply.Both || a.TypeToApply == Operator.TypesToApply.Numeric)); if (SelectedOperator == null) { SelectedOperator = Operators.FirstOrDefault(a => a.Title == "="); } NotifyPropertyChanged("SelectedOperator"); NotifyPropertyChanged("SelectedFeild"); _selectedFeild = value; NotifyPropertyChanged("SelectedFeildHasSetted"); } } public bool SelectedFeildHasSetted { get { return SelectedFeild != null && (SelectedAndOr.Name != "Suppress" || !IsOtherFilters); } } private ObservableCollection<Operator> _operators; private Feild _selectedFeild; private Operator _selectedOperator; private AndOr _selectedAndOr; private string _searchValue; private bool _isOtherFilters = true; } }
در گام بعد، یک کلاس کمکی برای سهولت ساخت عبارات ایجاد میکنیم:
using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; using AutoMapper; namespace DynamicSearch.ViewModel.Base { public static class ExpressionExtensions { public static List<T> CreateQuery<T>(Expression whereCallExpression, IQueryable entities) { return entities.Provider.CreateQuery<T>(whereCallExpression).ToList(); } public static MethodCallExpression CreateWhereCall<T>(Expression condition, ParameterExpression pe, IQueryable entities) { var whereCallExpression = Expression.Call( typeof(Queryable), "Where", new[] { entities.ElementType }, entities.Expression, Expression.Lambda<Func<T, bool>>(condition, new[] { pe })); return whereCallExpression; } public static void CreateLeftAndRightExpression<T>(string propertyName, Type type, string searchValue, ParameterExpression pe, out Expression left, out Expression right) { var typeOfNullable = type; typeOfNullable = typeOfNullable.IsNullableType() ? typeOfNullable.GetTypeOfNullable() : typeOfNullable; left = null; var typeMethodInfos = typeOfNullable.GetMethods(); var parseMethodInfo = typeMethodInfos.FirstOrDefault(a => a.Name == "Parse" && a.GetParameters().Count() == 1); var propertyInfos = typeof(T).GetProperties(); if (propertyName.Contains(".")) { left = CreateComplexTypeExpression(propertyName, propertyInfos, pe); } else { var propertyInfo = propertyInfos.FirstOrDefault(a => a.Name == propertyName); if (propertyInfo != null) left = Expression.Property(pe, propertyInfo); } if (left != null) left = Expression.Convert(left, typeOfNullable); if (parseMethodInfo != null) { var invoke = parseMethodInfo.Invoke(searchValue, new object[] { searchValue }); right = Expression.Constant(invoke, typeOfNullable); } else { //type is string right = Expression.Constant(searchValue.ToLower()); var methods = typeof(string).GetMethods(); var firstOrDefault = methods.FirstOrDefault(a => a.Name == "ToLower" && !a.GetParameters().Any()); if (firstOrDefault != null) left = Expression.Call(left, firstOrDefault); } } public static Expression CreateComplexTypeExpression(string searchFilter, IEnumerable<PropertyInfo> propertyInfos, Expression pe) { Expression ex = null; var infos = searchFilter.Split('.'); var enumerable = propertyInfos.ToList(); for (var index = 0; index < infos.Length - 1; index++) { var propertyInfo = infos[index]; var nextPropertyInfo = infos[index + 1]; if (propertyInfos == null) continue; var propertyInfo2 = enumerable.FirstOrDefault(a => a.Name == propertyInfo); if (propertyInfo2 == null) continue; var val = Expression.Property(pe, propertyInfo2); var propertyInfos3 = propertyInfo2.PropertyType.GetProperties(); var propertyInfo3 = propertyInfos3.FirstOrDefault(a => a.Name == nextPropertyInfo); if (propertyInfo3 != null) ex = Expression.Property(val, propertyInfo3); } return ex; } public static Expression AddOperatorExpression(Func<Expression, Expression, Expression> func, Expression left, Expression right) { return func.Invoke(left, right); } public static Expression JoinExpressions(bool isFirst, Func<Expression, Expression, Expression> func, Expression expression, Expression ex) { if (!isFirst) { return func.Invoke(expression, ex); } expression = ex; return expression; } } }
using System.Collections.ObjectModel; using System.Linq; using DynamicSearch.Model; using DynamicSearch.Service; using DynamicSearch.ViewModel.Base; namespace DynamicSearch.ViewModel { public class StudentSearchFilter : SearchFilterBase<Student> { public override ObservableCollection<Feild> Feilds { get { return new ObservableCollection<Feild> { new Feild("نام دانش آموز",typeof(string),"Name"), new Feild("نام خانوادگی دانش آموز",typeof(string),"Family"), new Feild("نام خانوادگی معلم",typeof(string),"Teacher.Name"), new Feild("شماره دانش آموزی",typeof(int),"StdID"), }; } } public override IQueryable<Student> GetQuarable() { return new StudentService().GetStudents().AsQueryable(); } } }
در نهایت زمل فایل موجود در پروژه ویو:
<Window x:Class="DynamicSearch.View.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:viewModel="clr-namespace:DynamicSearch.ViewModel;assembly=DynamicSearch.ViewModel" xmlns:view="clr-namespace:DynamicSearch.View" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Window.Resources> <viewModel:StudentSearchViewModel x:Key="StudentSearchViewModel" /> <view:VisibilityConverter x:Key="VisibilityConverter" /> </Window.Resources> <Grid DataContext="{StaticResource StudentSearchViewModel}"> <WrapPanel Orientation="Vertical"> <DataGrid AutoGenerateColumns="False" Name="asd" CanUserAddRows="False" ItemsSource="{Binding BindFilter}"> <DataGrid.Columns> <DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding AndOrs}" Visibility="{Binding IsOtherFilters,Converter={StaticResource VisibilityConverter}}" SelectedItem="{Binding SelectedAndOr,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn > <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding Feilds}" SelectedItem="{Binding SelectedFeild,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged }"/> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <ComboBox MinWidth="100" DisplayMemberPath="Title" ItemsSource="{Binding Operators}" IsEnabled="{Binding SelectedFeildHasSetted}" SelectedItem="{Binding SelectedOperator,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" /> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> <DataGridTemplateColumn Width="*"> <DataGridTemplateColumn.CellTemplate> <DataTemplate DataType="{x:Type viewModel:StudentSearchFilter}"> <TextBox IsEnabled="{Binding SelectedFeildHasSetted}" MinWidth="200" Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/> <!--<TextBox Text="{Binding SearchValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"/>--> </DataTemplate> </DataGridTemplateColumn.CellTemplate> </DataGridTemplateColumn> </DataGrid.Columns> </DataGrid> <Button Content="+" HorizontalAlignment="Left" Command="{Binding AddFilter}"/> <Button Content="Result" Command="{Binding ExecuteSearchFilter}"/> <DataGrid ItemsSource="{Binding Results}"> </DataGrid> </WrapPanel> </Grid> </Window>
برخی منابع جهت آشنایی با Expression ها:
http://msdn.microsoft.com/en-us/library/bb882637.aspx
انتخاب پویای فیلدها در LINQ
http://www.persiadevelopers.com/articles/dynamiclinqquery.aspx
نکته: کدهای نوشته شده در این مقاله، نسخههای نخستین هستند و طبیعتا جا برای بهبود بسیار دارند. دوستان میتوانند در این امر به بنده کمک کنند.
پیشنهادات جهت بهبود:
- جداسازی کدهای پیاده کننده منطق از ویو مدلها جهت افزایش قابلیت نگهداری کد و سهولت استفاده در سایر ساختارها
- افزودن توضیحات به کد
- انتخاب نامگذاریهای مناسب تر
DynamicSearch.zip
معماری لایه بندی شده، یک معماری بسیار همه گیر میباشد. به این خاطر که به راحتی SOC ، decoupling و قدرت درک کد را بسیار بالا میبرد. امروزه کمتر برنامه نویس و فعال حوضهی نرم افزاری است که با لایههای کلی و وظایف آنها آشنا نباشد ( UI layer آنچه که ما میبینیم، middle layer برای مقاصد منطق کاری، data access layer برای هندل کردن دسترسی به دادهها). اما مسئله ای که بیشتر برنامه نویسان و توسعه دهندگان نرم افزار با استانداردهای آن آشنا نیستند، راههای تبادل دادهها مابین layer ها میباشد. در این مقاله سعی داریم راههای تبادل دادهها را مابین لایهها، تشریح کنیم.
Layers و Tiers با هم متفاوت هستند
Layer با Tier متفاوت است. هنگامیکه در مورد مفهوم layer و Tier دچار شک شدید،
دیاگرام ذیل میتواند به شما بسیار کمک کند. layer به مجزاسازی
منطقی کد و Tier هم به مجزا سازی فیزیکی در ماشینهای مختلف اطلاق میشود. توجه داشته
باشید که این نکته یک شفاف سازی کلی در مورد یک مسئله مهم بود.
دادههای وارد شونده( incoming ) و خارج شونده( outgoing )
ما باید تبادل دادهها را از دو جنبه مورد بررسی قرار دهیم؛ اول اینکه دادهها چگونه به سمت لایه Data Access میروند، دوم اینکه دادهها چگونه به لایه UI پاس میشوند، در ادامه شما دلیل این مجزا سازی را درک خواهید کرد.
روش اول: Non-uniform
این روش اولین روش و احتمالا عمومیترین روش میباشد. خوب، اجازه دهید از لایهی UI به لایه DAL شروع کنیم. دادهها از لایه UI به Middle با استفاده از getter ها و setter ها ارسال خواهد شد. کد ذیل این مسئله را به روشنی نمایش میدهد.
Customer objCust = new Customer(); objCust.CustomerCode = "c001"; objCust.CustomerName = "Shivprasad";
بعد از آن، از لایه Middle به لایه Data Access دادهها با استفاده از مجزاسازی به وسیله comma و آرایهها و سایر روشهای non-uniform پاس داده میشوند. در کد ذیل به متد Add دقت کنید که چگونه فراخوانی به لایه Data Access را با استفاده از پارامترهای ورودی انجام میدهد.
public class Customer { private string _CustomerName = ""; private string _CustomerCode = ""; public string CustomerCode { get { return _CustomerCode; } set { _CustomerCode = value; } } public string CustomerName { get { return _CustomerName; } set { _CustomerName = value; } } public void Add() { CustomerDal obj = new CustomerDal(); obj.Add(_CustomerName,_CustomerCode); } }
کد ذیل، متد add در لایه Data Access را با استفاده از دو متد نمایش میدهد.
public class CustomerDal { public bool Add(string CustomerName,string CustomerCode) { // Insert data in to DB } }
بنابراین اگر بخواهیم به صورت خلاصه نحوه پاس دادن دادهها را در روش non-uniform بیان کنیم، شکل ذیل به زیبایی این مسئله را نشان میدهد.
· از لایه UI به لایه Middle با استفاده از setter و getter
· از لایه Middle به لایه data access با استفاده از comma ، input ، array
حال
نوبت این است بررسی کنیم که چگونه دادهها از DAL به UI در روش non-uniform پاس
خواهند شد. بنابراین اجازه دهید که اول از UI شروع
کنیم. از لایه UI دادهها با استفاده از object های لایه Middle واکشی میشوند.
Customer obj = new Customer(); List<Customer> oCustomers = obj.getCustomers();
از لایه Middle هم دادهها با استفاده از dataset ، datatable و xml پاس خواهند شد. مهمترین مسئله برای لایه middle ، loop بر روی dataset و تبدیل آن به strong type object ها میباشد. برای مثال میتوانید کد تابع getCustomers که بر روی dataset ، loop میزند و یک لیست از Customer ها را آماده میکند در ذیل مشاهده کنید. این تبدیل باید انجام شود، به این دلیل که UI به کلاسهای strongly typed دسترسی دارد.
public class Customer { private string _CustomerName = ""; private string _CustomerCode = ""; public string CustomerCode { get { return _CustomerCode; } set { _CustomerCode = value; } } public string CustomerName { get { return _CustomerName; } set { _CustomerName = value; } } public List<Customer> getCustomers() { CustomerDal obj = new CustomerDal(); DataSet ds = obj.getCustomers(); List<Customer> oCustomers = new List<Customer>(); foreach (DataRow orow in ds.Tables[0].Rows) { // Fill the list } return oCustomers; } }
با انجام این تبدیل به یکی از بزرگترین اهداف معماری لایه بندی شده میرسیم؛ یعنی اینکه « UI نمیتواند به طور مستقیم به کامپوننتهای لایه Data Access مانند ADO.NET ، OLEDB و غیره دستیابی داشته باشد. با این روش اگر ما در ادامه متدولوژی Data Access را تغییر دهیم تاثیری بر روی لایه UI نمیگذارد.» آخرین مسئله اینکه کلاس CustomerDal یک Dataset را با استفاده از ADO.NET بر میگرداند و Middle از آن استفاده میکند.
public class CustomerDal { public DataSet getCustomers() { // fetch customer records return new DataSet(); } }
حال اگر بخواهیم حرکت دادهها را به لایه UI، به صورت خلاصه بیان کنیم، شکل ذیل کامل این مسئله را نشان میدهد.
· دادهها از لایه DAL به لایه Middle با استفاده از Dataset ، Datareader ، XML ارسال خواهند شد.
· از لایه Middle به UI از strongly typed classes استفاده میشود.
مزایا و معایب روش non-uniform
یکی از مزایای non-uniform
· به راحتی قابل پیاده سازی میباشد، در مواردی که روش data access تغییر نمیکند این روش کارآیی لازم را دارد.
تعدادی از معایب این روش
· به خاطر اینکه یک ساختار uniform نداریم، بنابراین نیاز داریم که همیشه در هنگام عبور از یک لایه به یک لایهی دیگر از یک ساختار به یک ساختار دیگر تبدیل را انجام دهیم.
· برنامه نویسان از روشهای خودشان برای پاس دیتا استفاده میکنند؛ بنابراین این مسئله خود باعث پیچیدگی میشود.
· اگر برای مثال شما بخواهید متدولوژی Data Access خود را تغییر دهید، تغییرات بر تمام لایهها تاثیر میگذارد.
معرفی واژهی کلیدی جدید required در C# 11
فرض کنید یک Dto را به صورت زیر تعریف کردهاید و توسط یک API قرار است این اطلاعات را دریافت کنید:
public class OldCarDto { public string Brand { get; set; } public string Model { get; set; } public uint Horsepower { get; set; } }
var json = """ [ { "brand": "Ferrari", "horsePower": 651 }, { "model": "F50", "horsePower": 512 } ] """;
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; var oldResults = JsonSerializer.Deserialize<List<OldCarDto>>(json, options);
اگر اینبار تعریف Dto را به صورت زیر و بر اساس واژهی کلیدی جدید required اصلاح کنیم:
public class NewCarDto { public required string Brand { get; init; } public required string Model { get; init; } public required uint Horsepower { get; init; } }
var newResults = JsonSerializer.Deserialize<List<NewCarDto>>(json, options);
System.Text.Json.JsonException: JSON deserialization for type 'NewCarDto' was missing required properties, including the following: model
var blogs = from blog in Blogs where blog.Name.Contains("Development") select blog;
اما اکنون چطور؟
var blogs = from blog in Blogs where blog.Name.ComputeHash() == 0 select blog;
یک مثال: بررسی تاثیر ارزیابیهای سمت کلاینت در EF Core
فرض کنید ساختار جدول بلاگها به صورت زیر است:
public class Blog { public int BlogId { get; set; } public string Url { get; set; } }
public static class StringExtensions { public static int ComputeHash(this string str) { var hash = 0; foreach (var ch in str) { hash += (int)ch; } return hash; } }
using (var context = new BloggingContext()) { var blogs = context.Blogs .Where(blog => blog.Url.ComputeHash() >= 10) .ToList(); Console.WriteLine(blogs.First().Url); }
SELECT [blog].[BlogId], [blog].[Url] FROM [Blogs] AS [blog]
الف) مفسر LINQ در EF Core، شروع به ارزیابی کوئری نوشته شده میکند و هرجائیکه متدی را یافت و از درک آن عاجز بود (معادل SQL ایی را برای آن نیافت)، آنرا از کوئری حذف میکند.
ب) کوئری SQL نهایی بدون متد ComputeHash بر روی بانک اطلاعاتی اجرا شده و نتیجه به سمت کلاینت بازگشت داده میشود. به همین جهت است که در خروجی SQL فوق خبری از متد ComputeHash نیست.
ج) اکنون که EF Core اطلاعات لازم را از سمت سرور دریافت کردهاست، متد ComputeHash را در سمت کلاینت بر روی این نتیجهی دریافتی اعمال میکند. یعنی مرحلهی آخر همان LINQ to Objects متداول خواهد بود.
به این ترتیب است که EF Core قابلیت اجرای هر نوع متدی را که معادل SQL ایی برای آن وجود ندارد، خواهد یافت.
چگونه متوجه شویم که ارزیابی سمت کلاینت رخ دادهاست؟
EF Core این قابلیت را دارد تا گزارش کاملی را از ارزیابیهای سمت کلاینت صورت گرفته ارائه دهد. هرچند در مثال فوق متد الحاقی ComputeHash بسیار واضح است، اما برای نمونه متد string.Join نیز معادل SQL ایی ندارد:
var idUrls = context.Blogs .Select(b => new { IdUrlString = string.Join(", ", b.BlogId, b.Url), }).ToList();
public class BloggingContext : DbContext { public BloggingContext() { } public BloggingContext(DbContextOptions options) : base(options) { } public DbSet<Blog> Blogs { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Demo.ClientSideEvaluation;Trusted_Connection=True;"); optionsBuilder.ConfigureWarnings(warnings => { warnings.Log(CoreEventId.IncludeIgnoredWarning); warnings.Log(RelationalEventId.QueryClientEvaluationWarning); }); } } }
warn: Microsoft.EntityFrameworkCore.Query[200500] The LINQ expression 'where ([blog].Url.ComputeHash() >= 10)' could not be translated and will be evaluated locally.
اگر میخواهید ارزیابی سمت کلاینت را ممنوع کنید، در تنظیمات فوق warnings.Log را به warnings.Throw تغییر دهید. این مورد سبب خواهد شد تا اگر برنامه به این نوع ارزیابیها رسید، با یک استثناء متوقف شود (شبیه به حالت EF 6.x).
تاثیر ارزیابیهای سمت کلاینت بر روی کارآیی برنامه
هرچند قابلیت ارزیابیهای سمت کلاینت بسیار مفید است اما باید دقت داشت:
الف) در این حالت چون ابتدا متدهایی که قابلیت ارزیابی در سمت سرور را دارا نیستند، حذف خواهند شد، ممکن است تمام رکوردها به سمت کلاینت بازگشت داده شده و سپس فیلترینگ نهایی در سمت کلاینت صورت گیرد. مانند مثال محاسبهی hash که در SQL تولیدی آن، خبری از قسمت where نیست و این شرط در انتهای کار، در سمت کلاینت و به صورت LINQ to Objects اعمال میشود.
ب) این قابلیت ممکن است برنامه نویسها را از تفکر در مورد یافتن روشهای محاسباتی سمت سرور دور کند. برای مثال هر چند مثال string.Join نوشته شده در سمت کلاینت محاسبه خواهد شد و این کوئری بدون مشکل اجرا میشود، اما اگر آنرا به صورت ذیل جایگزین کنیم:
var idUrls2 = context.Blogs .Select(b => new { IdUrlString = b.BlogId + "," + b.Url }).ToList();
SELECT (CAST([b].[BlogId] AS nvarchar(max)) + N',') + [b].[Url] AS [IdUrlString] FROM [Blogs] AS [b]
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: ClientSideEvaluation.zip
هنگامی که حجم دادهها کم باشد شاید روش دسترسی و الگوریتم مورد استفاده اهمیتی نداشته باشد اما با افزایش حجم دادهها روشهای بهینهتر تاثیر مستقیم در کارایی برنامه دارند.
در این مثال سعی بر این است که در یک سناریوی خاص تفاوت بین Dictionary و List را بررسی کنیم :
فرض کنید 2 کلاس Student و Grade موجود است که وظیفهی نگهداری اطلاعات دانش آموز و نمره را بر عهده دارند.
public class Grade { public Guid StudentId { get; set; } public string Value { get; set; } public static IEnumerable<Grade> GetData() { for (int i = 0; i < 10000; i++) { yield return new Grade { StudentId = GuidHelper.ListOfIds[i], Value = "Value " + i }; } } } public class Student { public Guid Id { get; set; } public string Name { get; set; } public string Grade { get; set; } public static IEnumerable<Student> GetStudents() { for (int i = 0; i < 10000; i++) { yield return new Student { Id = GuidHelper.ListOfIds[i], Name = "Name " + i }; } } }
public class GuidHelper { public static List<Guid> ListOfIds=new List<Guid>(); static GuidHelper() { for (int i = 0; i < 10000; i++) { ListOfIds.Add(Guid.NewGuid()); } } }
ابتدا از LINQ روی لیست برای پیدا کردن نمرهی مورد نظر استفاده کرده و در روش دوم برای پیدا کردن نمرهی هر دانش آموز از Dictionary استفاده شده :
internal class Program { private static void Main(string[] args) { var stopwatch = new Stopwatch(); List<Grade> grades = Grade.GetData().ToList(); List<Student> students = Student.GetStudents().ToList(); stopwatch.Start(); foreach (Student student in students) { student.Grade = grades.Single(x => x.StudentId == student.Id).Value; } stopwatch.Stop(); Console.WriteLine("Using list {0}", stopwatch.Elapsed); stopwatch.Reset(); students = Student.GetStudents().ToList(); stopwatch.Start(); Dictionary<Guid, string> dictionary = Grade.GetData().ToDictionary(x => x.StudentId, x => x.Value); foreach (Student student in students) { student.Grade = dictionary[student.Id]; } stopwatch.Stop(); Console.WriteLine("Using dictionary {0}", stopwatch.Elapsed); Console.ReadKey(); } }
همانگونه که مشاهده میشود در این سناریو خواندن نمره از روی Dictionary بر اساس 'کلید' بسیار سریعتر از انجام یک پرس و جوی LINQ روی لیست است.
زمانی که از LINQ on list
student.Grade = grades.Single(x => x.StudentId == student.Id).Value;
زمانی که از Dictonary
student.Grade = dictionary[student.Id];
در نتیجه اگر نیاز به پیدا کردن اطلاعات بر اساس یک مقدار یکتا یا کلید باشد تبدیل اطلاعات به Dictionary و خواندن از آن بسیار به صرفهتر است.
تفاوت این 2 روش وقتی مشخص میشود که میزان دادهها زیاد باشد.
در همین رابطه (1 ، 2)
DictionaryVsList.zip
public class Rectangle { public int Width { get; set; } public int Height { get; set; } } public class Square:Rectangle { //codes specific to //square will be added }
Rectangle o = new Rectangle(); o.Width = 5; o.Height = 6;
Rectangle o = new Square(); o.Width = 5; o.Height = 6;
public class Square : Rectangle { public override int Width { get{return base.Width;} set { base.Height = value; base.Width = value; } } public override int Height { get{return base.Height;} set { base.Height = value; base.Width = value; } } }
public abstract class Shape { public virtual int Width { get; set; } public virtual int Height { get; set; } }
Shape o = new Rectangle(); o.Width = 5; o.Height = 6; Shape o = new Square(); o.Width = 5; //both height and width become 5 o.Height = 6; //both height and width become 6
برای مثال، جدول Post مدل بلاگ را در نظر بگیرید که در آن دو فیلد Body و Image تعریف شده اند.فیلد Body از نوع nvarchar max و فیلد Image از نوع varbinary max است و بدیهی است که این دو داده، به هنگام واکشی حافظهی زیادی مصرف میکنند.موارد بسیاری وجود دارند که ما به اطلاعات این دو فیلد احتیاجی نداریم از جمله: نمایش پستهای پر بازدید، پسته هایی که اخیرا ارسال شده اند و اصولا ما فقط به چند فیلد جدول Post احتیاج داریم و نه همهی آن ها.
namespace SplittingTableSample.DomainClasses { public class Post { public virtual int Id { get; set; } public virtual string Title { get; set; } public virtual DateTime CreatedDate { get; set; } public virtual string Body { get; set; } public virtual byte[] Image { get; set; } } }
دلیل اینکه در مدل فوق، تمامی خواص به صورت virtual تعریف شده اند، فعال سازی پروکسیهای ردیابی تغییر است. اگر دستور زیر را برای واکشی اطلاعات post با id=1 انجام دهیم:
using (var context = new MyDbContext()) { var post = context.Posts.Find(1); }
exec sp_executesql N'SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate], [Extent1].[Body] AS [Body], [Extent1].[Image] AS [Image] FROM [dbo].[Posts] AS [Extent1] WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
همان طور که مشاهده میکنید، با اجرای دستور فوق تمامی فیلدهای جدول Posts که id آنها برابر 1 بود واکشی شدند، ولی من تنها به فیلدهای Id و Title آن احتیاج داشتم. خب شاید بگویید که من به سادگی با projection، این مشکل را حل میکنم و تنها از فیلد هایی که به آنها احتیاج دارم، کوئری میگیرم. همهی اینها درست، اما projection هم مشکلات خود را دارد،به صورت پیش فرض، نوع بدون نام بر میگرداند و اگر بخواهیم این گونه نباشد، باید مقادیر آن را به یک کلاس(مثلا viewmodel) نگاشت کنیم و کلی مشکل دیگر.
راه حل دیگری که برای حل این مشکل ارائه میشود و برای نرمال سازی جداول نیز کاربرد دارد این است که، جدول Posts را به دو جدول مجزا که با یکدیگر رابطهی یک به یک دارند تقسیم کنیم، فیلدهای پر مصرف را در یک جدول و فیلدهای حجیم و کم مصرف را در جدول دیگری تعریف کنیم و سپس یک رابطهی یک به یک بین آن دو برقرار میکنیم.
به طور مثال این کار را بر روی جدول Posts ، به شکل زیر انجام شده است:
namespace SplittingTableSample.DomainClasses { public class Post { public virtual int Id { get; set; } public virtual string Title { get; set; } public virtual DateTime CreatedDate { get; set; } public virtual PostMetaData PostMetaData { get; set; } } } namespace SplittingTableSample.DomainClasses { public class PostMetaData { public virtual int PostId { get; set; } public virtual string Body { get; set; } public virtual byte[] Image { get; set; } public virtual Post Post { get; set; } } }
namespace SplittingTableSample.DomainClasses { public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData> { public PostMetaDataConfig() { HasKey(x => x.PostId); HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData); } } }
var post = context.Posts.Find(1);
exec sp_executesql N'SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate] FROM [dbo].[Posts] AS [Extent1] WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
اما راه حل ذکر شده نیز کاملا بدون ایراد نیست. مشکل اساسی آن تعدد تعداد جداول آن است. آیا جدول Post ، واقعا احتیاج به چنین سطح نرمال سازی و تبدیل آن به دو جدول مجزا را داشت؟ مطمئنا خیر. آیا واقعا راه حلی وجود دارد که ما در سمت کدهای خود با دو موجودیت مجزا کار کنیم، در صورتی که در دیتابیس این دو موجودیت، ساختار یک جدول را تشکیل دهند. در اینجا روشی مطرح میشود به نام تقسیم جدول (Table Splitting).
برای انجام این کار فقط چند تنظیم ساده لازم است:
1) فیلدهای موجودیت مورد نظر را به موجودیتهای کوچکتر، نگاشت میکنیم.
2) بین موجودیتهای کوچک تر، رابطهی یک به یک که هر دو سر رابطه Required هستند، رابطه برقرار میکنم.
3) با استفاده از Fluent API یا DataAnnotations، تمامی موجودیتها را به یک نام در دیتابیس نگاشت میکنیم.
برای مثال، تنظیمات Fluent برای کلاس Post و PostMetaData که رابطهی بین آنها یک به یک است را مشاهده میکنید:
namespace SplittingTableSample.DomainClasses { public class PostConfig : EntityTypeConfiguration<Post> { public PostConfig() { ToTable("Posts"); } } } namespace SplittingTableSample.DomainClasses { public class PostMetaDataConfig : EntityTypeConfiguration<PostMetaData> { public PostMetaDataConfig() { ToTable("Posts"); HasKey(x => x.PostId); HasRequired(x => x.Post).WithRequiredDependent(x => x.PostMetaData); } } }
اگر دستورات زیر را اجرا کنید:
var post = context.Posts.Find(1); Console.WriteLine(post.PostMetaData.Body);
برای متد Find خروجی زیر:
exec sp_executesql N'SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate] FROM [dbo].[Posts] AS [Extent1] WHERE [Extent1].[Id] = @p0',N'@p0 int',@p0=1
exec sp_executesql N'SELECT [Extent1].[Id] AS [Id], [Extent1].[Body] AS [Body], [Extent1].[Image] AS [Image] FROM [dbo].[Posts] AS [Extent1] WHERE [Extent1].[Id] = @EntityKeyValue1',N'@EntityKeyValue1 int',@EntityKeyValue1=1
حال اگر بخواهیم با یک رفت و آمد به دیتابیس کلیه اطلاعات را واکشی کنیم، میتوانیم از Eager Loading استفاده کنیم:
var post = context.Posts.Include(x => x.PostMetaData).SingleOrDefault(x => x.Id == 1);
SELECT [Limit1].[Id] AS [Id], [Limit1].[Title] AS [Title], [Limit1].[CreatedDate] AS [CreatedDate], [Extent2].[Id] AS [Id1], [Extent2].[Body] AS [Body], [Extent2].[Image] AS [Image] FROM (SELECT TOP (2) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[CreatedDate] AS [CreatedDate] FROM [dbo].[Posts] AS [Extent1] WHERE 1 = [Extent1].[Id] ) AS [Limit1] LEFT OUTER JOIN [dbo].[Posts] AS [Extent2] ON [Limit1].[Id] = [Extent2].[Id]
دریافت کدهای این بخش: SplittingTable-Sample.rar