EF Code First #8
using System.Collections.Generic; namespace EF_Sample04.Models { public class Employee { public int Id { set; get; } public string FirstName { get; set; } public string LastName { get; set; } public int? ManagerID { get; set; } public virtual Employee Manager { get; set; } } }
using EF_Sample04.Models; using System.Data.Entity.ModelConfiguration; namespace EF_Sample04.Mappings { public class EmployeeConfig : EntityTypeConfiguration<Employee> { public EmployeeConfig() { this.HasOptional(x => x.Manager) .WithMany() .HasForeignKey(x => x.ManagerID) .WillCascadeOnDelete(false); } } }
public SettingsController(ISettings settings) { // example of saving _settings.General.SiteName = "دات نت تیپس"; _settings.Seo.HomeMetaTitle = ".Net Tips"; _settings.Seo.HomeMetaKeywords = "َAsp.net MVC,Entity Framework,Reflection"; _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه"; _settings.Save(); }
- تنظیمات به صورت گروه بندی شده در کنار هم قرار گرفتهاند و یافتن تنظیمات برای زمانی که نیاز به دسترسی به آنها داریم، راحتتر و سادهتر خواهد بود.
- به این شکل تنظیمات قابل دسترس در یک گروه، از دیتابیس بازیابی خواهند شد.
اصلا چرا باید این تنظیمات را در دیتابیس ذخیره کنیم؟
- ساخت یک Asp.net Web Application
- ساخت مدل Setting و افزودن آن به کانتکست Entity Framework
- ساخت کلاس SettingBase برای بازیابی و ذخیره سازی تنظیمات با رفلکشن
- ساخت کلاس GenralSettins و SeoSettings که از کلاس SettingBase ارث بری کردهاند.
- ساخت کلاس Settings به منظور مدیریت تمام انواع تنظیمات
namespace DynamicSettingAPI.Models { public interface IUnitOfWork { DbSet<Setting> Settings { get; set; } int SaveChanges(); } } public class ApplicationDbContext : IdentityDbContext<ApplicationUser>,IUnitOfWork { public DbSet<Setting> Settings { get; set; } public ApplicationDbContext() : base("DefaultConnection", throwIfV1Schema: false) { } public static ApplicationDbContext Create() { return new ApplicationDbContext(); } } namespace DynamicSettingAPI.Models { public class Setting { public string Name { get; set; } public string Type { get; set; } public string Value { get; set; } } }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Setting>() .HasKey(x => new { x.Name, x.Type }); modelBuilder.Entity<Setting>() .Property(x => x.Value) .IsOptional(); base.OnModelCreating(modelBuilder); }
namespace DynamicSettingAPI.Service { public abstract class SettingsBase { //1 private readonly string _name; private readonly PropertyInfo[] _properties; protected SettingsBase() { //2 var type = GetType(); _name = type.Name; _properties = type.GetProperties(); } public virtual void Load(IUnitOfWork unitOfWork) { //3 get setting for this type name var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList(); foreach (var propertyInfo in _properties) { //get the setting from setting list var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name); if (setting != null) { //4 set propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType)); } } } public virtual void Save(IUnitOfWork unitOfWork) { //5 get all setting for this type name var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList(); foreach (var propertyInfo in _properties) { var propertyValue = propertyInfo.GetValue(this, null); var value = (propertyValue == null) ? null : propertyValue.ToString(); var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name); if (setting != null) { // 6 update existing value setting.Value = value; } else { // 7 create new setting var newSetting = new Setting() { Name = propertyInfo.Name, Type = _name, Value = value, }; unitOfWork.Settings.Add(newSetting); } } } } }
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
public class GeneralSettings : SettingsBase { public string SiteName { get; set; } public string AdminEmail { get; set; } public bool RegisterUsersEnabled { get; set; } } public class GeneralSettings : SettingsBase { public string SiteName { get; set; } public string AdminEmail { get; set; } }
public interface ISettings { GeneralSettings General { get; } SeoSettings Seo { get; } void Save(); } public class Settings : ISettings { // 1 private readonly Lazy<GeneralSettings> _generalSettings; // 2 public GeneralSettings General { get { return _generalSettings.Value; } } private readonly Lazy<SeoSettings> _seoSettings; public SeoSettings Seo { get { return _seoSettings.Value; } } private readonly IUnitOfWork _unitOfWork; public Settings(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; // 3 _generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>); _seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>); } public void Save() { // only save changes to settings that have been loaded if (_generalSettings.IsValueCreated) _generalSettings.Value.Save(_unitOfWork); if (_seoSettings.IsValueCreated) _seoSettings.Value.Save(_unitOfWork); _unitOfWork.SaveChanges(); } // 4 private T CreateSettings<T>() where T : SettingsBase, new() { var settings = new T(); settings.Load(_unitOfWork); return settings; } }
private readonly ICache _cache; public Settings(IUnitOfWork unitOfWork, ICache cache) { // ARGUMENT CHECKING SKIPPED FOR BREVITY _unitOfWork = unitOfWork; _cache = cache; _generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>); _seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>); } private T CreateSettingsWithCache<T>() where T : SettingsBase, new() { // this is where you would implement loading from ICache throw new NotImplementedException(); }
public ActionResult Index() { using (var uow = new ApplicationDbContext()) { var _settings = new Settings(uow); _settings.General.SiteName = "دات نت تیپس"; _settings.General.AdminEmail = "admin@gmail.com"; _settings.General.RegisterUsersEnabled = true; _settings.Seo.HomeMetaTitle = ".Net Tips"; _settings.Seo.MetaKeywords = "Asp.net MVC,Entity Framework,Reflection"; _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه"; var settings2 = new Settings(uow); var output = string.Format("SiteName: {0} HomeMetaDescription: {1} MetaKeywords: {2} MetaTitle: {3} RegisterEnable: {4}", settings2.General.SiteName, settings2.Seo.HomeMetaDescription, settings2.Seo.MetaKeywords, settings2.Seo.HomeMetaTitle, settings2.General.RegisterUsersEnabled.ToString() ); return Content(output); } }
روشهای زیادی برای مدیریت این مساله وجود دارند؛ مانند استفاده از ماژولهای URL Rewrite برای بازنویسی آدرسهای نهایی صفحهی در حال رندر و یا ... به روز رسانی مستقیم بانک اطلاعاتی، یافتن تمام فیلدهای رشتهای ممکن در تمام جداول موجود و سپس اعمال تغییرات.
یافتن لیست تمام جداول قابل مدیریت توسط Entity framework
در ابتدا میخواهیم لیست پویای تمام جداول مدیریت شدهی توسط EF را پیدا کنیم. از این جهت که نمیخواهیم به ازای هر کدام یک کوئری جداگانه بنویسیم.
using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using EFReplaceAll.Models; namespace EFReplaceAll.Config { public class DbSetInfo { public IQueryable<object> DbSet { set; get; } public Type DbSetType { set; get; } } public class MyContext : DbContext { public DbSet<Product> Products { set; get; } public DbSet<Category> Categories { set; get; } public DbSet<User> Users { set; get; } public MyContext() : base("Connection1") { this.Database.Log = sql => Console.Write(sql); } public IList<DbSetInfo> GetAllDbSets() { return this.GetType() .GetProperties() .Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) .Select(p => new DbSetInfo { DbSet = (IQueryable<object>)p.GetValue(this, null), DbSetType = p.PropertyType.GetGenericArguments().First() }) .ToList(); } } }
یافتن فیلدهای رشتهای رکوردهای تمام جداول و سپس به روز رسانی آنها
میخواهیم متدی را طراحی کنیم که در آن لیستی از یافتنها و جایگزینیها قابل تعیین باشد. به همین جهت مدل زیر را تعریف میکنیم:
using System; namespace EFReplaceAll.Utils { public class ReplaceOp { public string ToFind { set; get; } public string ToReplace { set; get; } public StringComparison Comparison { set; get; } } }
سپس متدی که کار یافتن تمام فیلدهای رشتهای و سپس جایگزین کردن آنها را انجام میدهد به صورت زیر خواهد بود:
using System.Collections.Generic; using System.Linq; using EFReplaceAll.Config; namespace EFReplaceAll.Utils { public static class UpdateDbContextContents { public static void ReplaceAllStringsAcrossTables(IList<ReplaceOp> replaceOps) { int dbSetsCount; using (var uow = new MyContext()) { dbSetsCount = uow.GetAllDbSets().Count; } for (var i = 0; i < dbSetsCount; i++) { using (var uow = new MyContext()) // using a new context each time to free resources quickly. { var dbSetResult = uow.GetAllDbSets()[i]; var stringProperties = dbSetResult.DbSetType.GetProperties() .Where(p => p.PropertyType == typeof(string)) .ToList(); var dbSetEntities = dbSetResult.DbSet; var haveChanges = false; foreach (var entity in dbSetEntities) { foreach (var stringProperty in stringProperties) { var oldPropertyValue = stringProperty.GetValue(entity, null) as string; if (string.IsNullOrWhiteSpace(oldPropertyValue)) { continue; } var newPropertyValue = oldPropertyValue; foreach (var replaceOp in replaceOps) { newPropertyValue = newPropertyValue.ReplaceString(replaceOp.ToFind, replaceOp.ToReplace, replaceOp.Comparison); } if (oldPropertyValue != newPropertyValue) { stringProperty.SetValue(entity, newPropertyValue, null); haveChanges = true; } } } if (haveChanges) { uow.SaveChanges(); } } } } } }
- در اینجا using (var uow = new MyContext()) را زیاد مشاهده میکنید. علت اینجا است که اگر تنها با یک Context کار کنیم، EF تمام تغییرات و تمام رکوردهای وارد شدهی به آنرا کش میکند و مصرف حافظهی برنامه با توجه به خواندن تمام رکوردهای بانک اطلاعاتی توسط آن، ممکن است به چند گیگابایت برسد. به همین جهت از Contextهایی با طول عمر کوتاه استفاده شدهاست تا میزان مصرف RAM این متد سبب کرش برنامه نشود.
- در ابتدای کار توسط متد GetAllDbSets که به Context اضافه کردیم، تعداد DbSetهای موجود را پیدا میکنیم تا بتوان بر روی آنها حلقهای را تشکیل داد و به ازای هر کدام یک (()using (var uow = new MyContext را تشکیل داد.
- سپس با استفاده از نوع DbSet که توسط خاصیت dbSetResult.DbSetType در دسترس است، خواص رشتهای ممکن این DbSet یافت میشوند.
- در ادامه dbSetResult.DbSet یک Data Reader را به صورت پویا بر روی DbSet جاری باز کرده و تمام رکوردهای این DbSet را یک به یک بازگشت میدهد.
- در اینجا با استفاده از Reflection، از رکورد جاری، مقادیر خواص رشتهای آن دریافت شده و سپس کار جستجو و جایگزینی انجام میشود.
- در آخر هم فراخوانی uow.SaveChanges کار ثبت تغییرات صورت گرفته را انجام میدهد.
متدی برای جایگزینی غیرحساس به حروف بزرگ و کوچک
متد استاندارد Replace رشتهها، حساس به حروف بزرگ و کوچک است. یک نمونهی عمومیتر را که در آن بتوان StringComparison.OrdinalIgnoreCase را تعیین کرد، در ذیل مشاهده میکنید که از آن در متد ReplaceAllStringsAcrossTables فوق استفاده شدهاست:
using System; using System.Text; namespace EFReplaceAll.Utils { public static class StringExtensions { public static string ReplaceString(this string src, string oldValue, string newValue, StringComparison comparison) { if (string.IsNullOrWhiteSpace(src)) { return src; } if (string.Compare(oldValue, newValue, comparison) == 0) { return src; } var sb = new StringBuilder(); var previousIndex = 0; var index = src.IndexOf(oldValue, comparison); while (index != -1) { sb.Append(src.Substring(previousIndex, index - previousIndex)); sb.Append(newValue); index += oldValue.Length; previousIndex = index; index = src.IndexOf(oldValue, index, comparison); } sb.Append(src.Substring(previousIndex)); return sb.ToString(); } } }
UpdateDbContextContents.ReplaceAllStringsAcrossTables( new[] { new ReplaceOp { ToFind = "https://www.dntips.ir", ToReplace = "https://www.dntips.ir", Comparison = StringComparison.OrdinalIgnoreCase }, new ReplaceOp { ToFind = "https://www.dntips.ir", ToReplace = "https://www.dntips.ir", Comparison = StringComparison.OrdinalIgnoreCase } });
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: EFReplaceAll.zip
قسمت اول : تبادل دادهها بین لایه ها- قسمت اول
روش دوم: Uniform(Entity classes)
روش دیگر پاس دادن دادهها، روش uniform است. در این روش کلاسهای Entity، یک سری کلاس ساده به همراه یکسری Property های Get و Set میباشند. این کلاسها شامل هیچ منطق کاری نمیباشند. برای مثال کلاس CustomerEntity که دارای دو Property ، Customer Name و Customer Code میباشد. شما میتوانید تمام Entity ها را به صورت یک پروژهی مجزا ایجاد کرده و به تمام لایهها رفرنس دهید.
public class CustomerEntity { protected string _CustomerName = ""; protected string _CustomerCode = ""; public string CustomerCode { get { return _CustomerCode; } set { _CustomerCode = value; } } public string CustomerName { get { return _CustomerName; } set { _CustomerName = value; } } }
خوب، اجازه دهید تا از CustomerDal شروع کنیم. این کلاس یک Collection از CustomerEntity را بر میگرداند و همچنین یک CustomerEntity را برای اضافه کردن به دیتابیس . توجه داشته باشید که لایه Data Access وظیفه دارد تا دیتای دریافتی از دیتابیس را به CustomerEntity تبدیل کند.
public class CustomerDal { public List<CustomerEntity> getCustomers() { // fetch customer records return new List<CustomerEntity>(); } public bool Add(CustomerEntity obj) { // Insert in to DB return true; } }
لایه Middle از CustomerEntity ارث بری میکند و یکسری operation را به entity class اضافه خواهد کرد. دادهها در قالب Entity Class به لایه Data Access ارسال میشوند و در همین قالب نیز بازگشت داده میشوند. این مسئله در کد ذیل به روشنی مشاهده میشود.
public class Customer : CustomerEntity { public List<CustomerEntity> getCustomers() { CustomerDal obj = new CustomerDal(); return obj.getCustomers(); } public void Add() { CustomerDal obj = new CustomerDal(); obj.Add(this); } }
لایه UI هم با تعریف یک Customer و فراخوانی operation های مربوط به آن، دادهی مد نظر خود را در قالب CustomerEntity بازیابی خواهد کرد. اگر بخواهیم عمکرد روش uniform را خلاصه کنیم باید بگوییم، در این روش دیتای رد و بدل شدهی مابین کلیه لایهها با یک ساختار استاندارد، یعنی Entity پاس داده میشوند.
مزایا و معایب روش uniform
مزایا
·Strongly typed به صورت در تمامی لایهها قابل دسترسی و استفاده میباشد.
· به دلیل اینکه از ساختار عمومی Entity استفاده میکند، بنابراین فقط یکبار نیاز به تبدیل دادهها وجود دارد. به این معنی که کافی است یک بار دیتای واکشی شده از دیتابیس را به یک ساختار Entity تبدیل کنید و در ادامه بدون هیچ تبدیل دیگری از این Entity استفاده کنید.
معایب
· تنها مشکلی که این روش دارد، مشکلی است به نام Double Loop . هنگامیکه شما در مورد کلاسهای entity بحث میکنید، ساختارهای دنیای واقعی را مدل میکنید. حال فرض کنید شما به دلیل یکسری مسایل فنی دیتابیس خود را Optimize کرده اید. بنابراین ساختار دنیای واقعی با ساختاری که شما در نرم افزار مدل کردهاید متفاوت میباشد. بگذارید یک مثال بزنیم؛ فرض کنید که یک customer دارید، به همراه یکسری Address. همان طور که ذکر کردیم، به دلیل برخی مسایل فنی ( denormalized ) به صورت یک جدول در دیتا بیس ذخیره شده است. بنابراین سرعت واکشی اطلاعات بیشتر است. اما خوب اگر ما بخواهیم این ساختار را در دنیای واقعی بررسی کنیم، ممکن است با یک ساختار یک به چند مانند شکل ذیل برخورد کنیم.
بنابراین مجبوریم یکسری کد جهت این تبدیل همانند کد ذیل بنویسیم.
foreach (DataRow o1 in oCustomers.Tables[0].Rows) { obj.Add(new CustomerEntyityAddress()); // Fills customer foreach (DataRow o in oAddress.Tables[0].Rows) { obj[0].Add(new AddressEntity()); // Fills address } }
یکی از مواردی که در تمام برنامههای فارسی "باید" رعایت شود (مهم نیست به چه زبانی یا چه سکویی باشد یا چه بانک اطلاعاتی مورد استفاده است)، بحث اصلاح "ی" و "ک" دریافتی از کاربر و یکسان سازی آنها میباشد. به عبارتی برنامهی فارسی که اصلاح خودکار این دو مورد را لحاظ نکرده باشد دیر یا زود به مشکلات حادی برخورد خواهد کرد و "ناقص" است : اطلاعات بیشتر ؛ برای مثال شاید دوست نداشته باشید که دو کامران در سایت شما ثبت نام کرده باشند؛ یکی با ک فارسی و یکی با ک عربی! به علاوه همین کامران امروز میتواند لاگین کند و فردا با یک کامپیوتر دیگر و صفحه کلیدی دیگر پشت درب خواهد ماند. در حالیکه از دید این کامران، کلمه کامران همان کامران است!
بنابراین در دو قسمت "باید" این یکسان سازی صورت گیرد:
الف) پیش از ثبت اطلاعات در بانک اطلاعاتی (تا با دو کامران ثبت شده در بانک اطلاعاتی مواجه نشوید)
ب) پیش از جستجو (تا کامران روزی دیگر با صفحه کلیدی دیگر بتواند به برنامه وارد شود)
راه حل یکسان سازی هم شاید به نظر این باشد: رخداد فشرده شدن کلید را کنترل کنید و سپس جایگزینی را انجام دهید (مثلا ی عربی را با ی فارسی جایگزین کنید). این روش چند ایراد دارد:
الف) Silverlight به دلایل امنیتی اصلا چنین اجازهای را به شما نمیدهد! (تا نتوان کلیدی را جعل کرد)
ب) همیشه با یک TextBox ساده سر و کار نداریم. کنترلهای دیگری هم هستند که امکان ورود اطلاعات در آنها وجود دارد و آن وقت باید برای تمام آنها کد نوشت. ظاهر کدهای برنامه در این حالت در حجم بالا، اصلا جالب نخواهد بود و ضمنا ممکن است یک یا چند مورد فراموش شوند.
راه بهتر این است که دقیقا حین ثبت اطلاعات یا جستجوی اطلاعات در لایهای که تمام ثبتها یا اعمال کار با بانک اطلاعاتی برنامه به آنجا منتقل میشود، کار یکسان سازی صورت گیرد. به این صورت کار یکپارچه سازی یکبار باید انجام شود اما تاثیرش را بر روی کل برنامه خواهد گذاشت، بدون اینکه هرجایی که امکان ورود اطلاعات هست روالهای رخداد گردان هم حضور داشته باشند.
در مورد مقدمات WCF RIA Services که درSilverlight و ASP.NET کاربرد دارد میتوانید به این مطلب مراجعه کنید: +
جهت تکمیل این بحث متدی تهیه شده که کار یکسان سازی ی و ک دریافتی از کاربر را حین ثبت توسط امکانات WCF RIA Services انجام میدهد (دقیقا پیش از فراخوانی متد SubmitChanges باید بکارگرفته شود):
namespace SilverlightTests.RiaYeKe
{
public static class PersianHelper
{
public static string ApplyUnifiedYeKe(this string data)
{
if (string.IsNullOrEmpty(data)) return data;
return data.Replace("ی", "ی").Replace("ک", "ک");
}
}
}
using System.Linq;
using System.Windows.Controls;
using System.Reflection;
using System.ServiceModel.DomainServices.Client;
namespace SilverlightTests.RiaYeKe
{
public class RIAHelper
{
/// <summary>
/// یک دست سازی ی و ک در عبارات ثبت شده در بانک اطلاعاتی پیش از ورود به آن
/// این متد باید پیش از فراخوانی متد
/// SubmitChanges
/// استفاده شود
/// </summary>
/// <param name="dds"></param>
public static void ApplyCorrectYeKe(DomainDataSource dds)
{
if (dds == null)
return;
if (dds.DataView.TotalItemCount <= 0)
return;
//پیدا کردن موجودیتهای تغییر کرده
var changedEntities = dds.DomainContext.EntityContainer.GetChanges().Where(
c => c.EntityState == EntityState.Modified ||
c.EntityState == EntityState.New);
foreach (var entity in changedEntities)
{
//یافتن خواص این موجودیتها
var propertyInfos = entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
);
foreach (var propertyInfo in propertyInfos)
{
//اگر این خاصیت رشتهای است ی و ک آن را استاندارد کن
if (propertyInfo.PropertyType != typeof (string)) continue;
var propName = propertyInfo.Name;
var val = new PropertyReflector().GetValue(entity, propName);
if (val == null) continue;
new PropertyReflector().SetValue(
entity,
propName,
val.ToString().ApplyUnifiedYeKe());
}
}
}
}
}
توضیحات:
از آنجائیکه حین فراخوانی متد SubmitChanges فقط موجودیتهای تغییر کرده جهت ثبت ارسال میشوند، ابتدا این موارد یافت شده و سپس خواص عمومی تک تک این اشیاء توسط عملیات Reflection بررسی میگردند. اگر خاصیت مورد بررسی از نوع رشتهای بود، یکبار این یک دست سازی اطلاعات ی و ک دریافتی صورت خواهد گرفت (و از آنجائیکه این تعداد همیشه محدود است عملیات Reflection سربار خاصی نخواهد داشت).
اگر در کدهای خود از DomainDataSource استفاده نمیکنید باز هم تفاوتی نمیکند. متد ApplyCorrectYeKe را از قسمت DomainContext.EntityContainer به بعد دنبال کنید.
اکنون تنها مورد باقیمانده بحث جستجو است که با اعمال متد ApplyUnifiedYeKe به مقدار ورودی متد جستجوی خود، مشکل حل خواهد شد.
کلاس PropertyReflector بکارگرفته شده هم از اینجا به عاریت گرفته شد.
دریافت کدهای این بحث
استفاده یا عدم استفاده از یک تکنولوژی یا ابزار خاص، به پارامترهای مختلفی از جمله ابعاد پروژه، مهارت و دانش اعضای تیم، ماهیت پروژه، پلتفرم اجرا، بودجهی پروژه، مهلت تکمیل پروژه و تعداد نفرات تیم بستگی دارد. بنابراین واضح است پیچیدن یک نسخهی خاص، برای همهی سناریوها امکان پذیر نیست؛ اما شرایطی وجود دارد که استفاده یا عدم استفاده از این ابزارهای تکنولوژیک منطقیتر مینمایند.
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 است و نه بیشتر؛ مگر در مواردی خاص که به تشخیص شما نیاز به استفاده بیشتر از آن وجود داشته باشد.
در این قسمت ابتدا نحوهی فعال سازی فریم ورک آزمونهای واحد مایکروسافت و سپس نحوهی فعال سازی این تامین کنندهی بانک اطلاعاتی درون حافظهای را بررسی خواهیم کرد. به علاوه برای سرویس بلاگهای قسمت قبل نیز آزمون واحد خواهیم نوشت.
نحوهی فعالسازی فریم ورک MSTest در یک پروژهی Class library از نوع NET Core.
تنها نکتهی مهم فعالسازی MSTest در یک پروژهی Class library جدید که برای نوشتن آزمونهای واحد مورد استفاده قرار خواهیم داد، تنظیمات فایل project.json آن است که در ذیل آمده است:
{ "version": "1.0.0-*", "testRunner": "mstest", "dependencies": { "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" }, "dotnet-test-mstest": "1.1.1-preview", "MSTest.TestFramework": "1.0.1-preview", "NETStandard.Library": "1.6.0", "Microsoft.EntityFrameworkCore": "1.0.0", "Microsoft.EntityFrameworkCore.InMemory": "1.0.0", "Core1RtmEmptyTest.DataLayer": "1.0.0-*", "Core1RtmEmptyTest.Entities": "1.0.0-*", "Core1RtmEmptyTest.Services": "1.0.0-*", "Core1RtmEmptyTest.ViewModels": "1.0.0-*" }, "frameworks": { "netcoreapp1.0": { "imports": [ "dnxcore50", "portable-net45+win8" ] } } }
- به علاوه در اینجا ارجاعاتی را به اسمبلیهای موجودیتها، Services و DataLayer که در قسمت «شروع به کار با EF Core 1.0 - قسمت 14 - لایه بندی و تزریق وابستگیها» بررسی شدند نیز ملاحظه میکنید.
- همچنین وابستگی جدید Microsoft.EntityFrameworkCore.InMemory نیز در اینجا قابل ملاحظه است. این وابستگی را تنها به پروژهی آزمونهای واحد خود اضافه میکنیم. از این جهت که تنظیمات آن صرفا در این قسمت جدید قید میشوند و نه در سایر قسمتهای برنامه.
پس از آن، کار با این فریم ورک، همانند سایر نگارشهای دات نت خواهد بود:
using Microsoft.VisualStudio.TestTools.UnitTesting; namespace EFCore.MsTests { [TestClass] public class CoreTests { [TestMethod] public void Test1() { Assert.IsTrue(true); } } }
پس از نوشتن اولین آزمون واحد، یکبار پروژه را build کرده و سپس از منوی Test، گزینهی Windows را انتخاب کرده و در اینجا گزینهی Test Explorer را انتخاب کنید. اندکی صبر کنید تا آزمونهای واحد شما شناسایی شوند و سپس گزینهی Run All را انتخاب کنید:
تغییرات Context برنامه جهت استفادهی از تامین کنندهی داخل حافظهای
در مورد نحوهی تعریف و افزودن وابستگیهای EF Core در مطلب «شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه» پیشتر بحث شد و همچنین در مطلب «شروع به کار با EF Core 1.0 - قسمت 3 - انتقال مهاجرتها به یک اسمبلی دیگر»، اطلاعات Context برنامه را به اسمبلی دیگری منتقل کردیم.
اگر از روش بازنویسی متد OnConfiguring برای تنظیم تامین کنندهی بانک اطلاعاتی مورد نظر استفاده میکنید، متد OnConfiguring کلاس Context برنامه چنین شکلی را پیدا میکند:
public class ApplicationDbContext : DbContext, IUnitOfWork { private readonly IConfigurationRoot _configuration; public ApplicationDbContext(IConfigurationRoot configuration) { _configuration = configuration; } public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { optionsBuilder.UseSqlServer( _configuration["ConnectionStrings:ApplicationDbContextConnection"] , serverDbContextOptionsBuilder => { var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds; serverDbContextOptionsBuilder.CommandTimeout(minutes); }); } }
الف) اضافه شدن سازندهی دومی که <DbContextOptions<ApplicationDbContext را دریافت میکند. از آن در سمت کدهای آزمون واحد برنامه جهت ثبت ()options.UseInMemoryDatabase استفاده میشود.
ب) به متد OnConfiguring، بررسی optionsBuilder.IsConfigured هم اضافه شدهاست. چون در سمت کدهای آزمون واحد، تامین کنندهی بانک اطلاعاتی درون حافظهای اضافه میشود، مقدار optionsBuilder.IsConfigured به true تنظیم خواهد شد و دیگر از تامین کنندهی SQL Server استفاده نمیشود.
اگر از متد OnConfiguring به این شکل استفاده نمیکنید، تنها ذکر سازندهی دوم ضروری است. از این جهت که در آزمونهای واحد، از تنظیمات متد ConfigureServices کلاس آغازین برنامه استفاده نخواهد شد.
نوشتن آزمونهای واحد مخصوص EF Core
پس از برپایی پیشنیازهای نوشتن آزمونها واحد، شامل تنظیمات فریم ورک MSTest و همچنین افزودن وابستگیهای مرتبط با فایل project.json ایی که در ابتدای بحث عنوان شد و اصلاح سازنده و متد OnConfiguring کلاس Context برنامه جهت آماده سازی آنها برای پذیرش تامین کنندههای دیگر، اکنون یک نمونه از آزمونهای واحد درون حافظهای EF Core، چنین شکلی را خواهد داشت:
using System; using System.Linq; using Core1RtmEmptyTest.DataLayer; using Core1RtmEmptyTest.Entities; using Core1RtmEmptyTest.Services; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Core1RtmEmptyTest.MsTests { [TestClass] public class CoreTests { private readonly IServiceProvider _serviceProvider; public CoreTests() { var services = new ServiceCollection(); services.AddEntityFrameworkInMemoryDatabase() .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase()); services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<IBlogService, BlogService>(); _serviceProvider = services.BuildServiceProvider(); } [TestMethod] public void Find_searches_url() { // Insert seed data into the database using one instance of the context using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) { context.Set<Blog>().Add(new Blog { Url = "http://sample.com/cats" }); context.Set<Blog>().Add(new Blog { Url = "http://sample.com/catfish" }); context.Set<Blog>().Add(new Blog { Url = "http://sample.com/dogs" }); context.SaveAllChanges(); } } // Use a separate instance of the context to verify correct data was saved to database using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) { Assert.AreEqual(3, context.Set<Blog>().Count()); Assert.AreEqual("http://sample.com/cats", context.Set<Blog>().First().Url); } } // Use a clean instance of the context to run the test using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { var blogService = serviceScope.ServiceProvider.GetRequiredService<IBlogService>(); var results = blogService.GetPagedBlogsAsNoTracking(pageNumber: 0, recordsPerPage: 10); Assert.AreEqual(3, results.Count); } } } }
همانطور که در قسمت «تغییرات Context برنامه جهت استفادهی از تامین کنندهی داخل حافظهای» فوق عنوان شد، در حین انجام آزمونهای واحد، دیگر به کلاس آغازین برنامه و تنظیمات آن مراجعه نمیشود. بنابراین باید شبیه به عملکرد متد ConfigureServices آنرا در اینجا پیاده سازی کرد. نمونهای از انجام اینکار را در سازندهی کلاس انجام آزمونهای واحد مشاهده میکنید:
private readonly IServiceProvider _serviceProvider; public CoreTests() { var services = new ServiceCollection(); services.AddEntityFrameworkInMemoryDatabase() .AddDbContext<ApplicationDbContext>(options => options.UseInMemoryDatabase()); services.AddScoped<IUnitOfWork, ApplicationDbContext>(); services.AddScoped<IBlogService, BlogService>(); _serviceProvider = services.BuildServiceProvider(); }
سپس همانند قبل، باید تمام سرویسهای مدنظر تنظیم شوند تا بتوان از آنها استفاده کرد.
نکتهی مهم دیگری را که باید به آن دقت داشت، ایجاد scope و سپس دسترسی به سرویسها از طریق این Scope است. از این جهت که چون خارج از طول عمر یک درخواست وب قرار داریم، دیگر Scopeها برای ما به صورت خودکار ایجاد و تخریب نمیشوند و باید همانکاری را که ASP.NET Core در پشت صحنه انجام میدهد، به صورت دستی پیاده سازی کنیم:
using (var serviceScope = _serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { using (var context = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWork>()) {
یک نکتهی تکمیلی
EF Core به همراه تامین کنندهی بانک اطلاعاتی SQLite نیز هست. یکی از نکات ویژهی بانک اطلاعاتی SQLite، امکان تنظیم پارامتری است در رشتهی اتصالی آن، که آنرا نیز تبدیل به یک «بانک اطلاعاتی درون حافظهای» میکند. این روش سالها است که جهت انجام آزمونهای واحد ORMها مورد استفاده قرار میگیرد. بنابراین میتوان آنرا به عنوان جایگزینی برای مطلب جاری نیز درنظر گرفت.
var connectionStringBuilder = new SqliteConnectionStringBuilder { DataSource = ":memory:" }; var connectionString = connectionStringBuilder.ToString(); var connection = new SqliteConnection(connectionString); services.AddEntityFrameworkSqlite().AddDbContext<CmsDbContext>(options => options.UseSqlite(connection));
EF Code First #14
ردیابی تغییرات در EF Code first
EF از DbContext برای ذخیره اطلاعات مرتبط با تغییرات موجودیتهای تحت کنترل خود کمک میگیرد. این نوع اطلاعات توسط Change Tracker API جهت بررسی وضعیت فعلی یک شیء، مقادیر اصلی و مقادیر تغییر کرده آن در دسترس هستند. همچنین در اینجا امکان بارگذاری مجدد اطلاعات موجودیتها از بانک اطلاعاتی جهت اطمینان از به روز بودن آنها تدارک دیده شده است. سادهترین روش دستیابی به این اطلاعات، استفاده از متد context.Entry میباشد که یک وهله از موجودیتی خاص را دریافت کرده و سپس به کمک خاصیت State خروجی آن، وضعیتهایی مانند Unchanged یا Modified را میتوان به دست آورد. علاوه بر آن خروجی متد context.Entry، دارای خواصی مانند CurrentValues و OriginalValues نیز میباشد. OriginalValues شامل مقادیر خواص موجودیت درست در لحظه اولین بارگذاری در DbContext برنامه است. CurrentValues مقادیر جاری و تغییر یافته موجودیت را باز میگرداند. به علاوه این خروجی امکان فراخوانی متد GetDatabaseValues را جهت بدست آوردن مقادیر جدید ذخیره شده در بانک اطلاعاتی نیز ارائه میدهد. ممکن است در این بین، خارج از Context جاری، اطلاعات بانک اطلاعاتی توسط کاربر دیگری تغییر کرده باشد. به کمک GetDatabaseValues میتوان به این نوع اطلاعات نیز دست یافت.
حداقل چهار کاربرد عملی جالب را از اطلاعات موجود در Change Tracker API میتوان مثال زد که در ادامه به بررسی آنها خواهیم پرداخت.
کلاسهای مدل مثال جاری
در اینجا یک رابطه many-to-one بین جدول هزینههای اقلام خریداری شده یک شخص و جدول فروشندگان تعریف شده است:
using System;
namespace EF_Sample09.DomainClasses
{
public abstract class BaseEntity
{
public int Id { get; set; }
public DateTime CreatedOn { set; get; }
public string CreatedBy { set; get; }
public DateTime ModifiedOn { set; get; }
public string ModifiedBy { set; get; }
}
}
using System;
namespace EF_Sample09.DomainClasses
{
public class Bill : BaseEntity
{
public decimal Amount { set; get; }
public string Description { get; set; }
public virtual Payee Payee { get; set; }
}
}
using System.Collections.Generic;
namespace EF_Sample09.DomainClasses
{
public class Payee : BaseEntity
{
public string Name { get; set; }
public virtual ICollection<Bill> Bills { set; get; }
}
}
به علاوه همانطور که ملاحظه میکنید، این کلاسها از یک abstract class به نام BaseEntity مشتق شدهاند. هدف از این کلاس پایه تنها تامین یک سری خواص تکراری در کلاسهای برنامه است و هدف از آن، مباحث ارث بری مانند TPH، TPT و TPC نیست.
به همین جهت برای اینکه این کلاس پایه تبدیل به یک جدول مجزا و یا سبب یکی شدن تمام کلاسها در یک جدول نشود، تنها کافی است آنرا به عنوان DbSet معرفی نکنیم و یا میتوان از متد Ignore نیز استفاده کرد:
using System.Data.Entity;
using EF_Sample09.DomainClasses;
namespace EF_Sample09.DataLayer.Context
{
public class Sample09Context : MyDbContextBase
{
public DbSet<Bill> Bills { set; get; }
public DbSet<Payee> Payees { set; get; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Ignore<BaseEntity>();
base.OnModelCreating(modelBuilder);
}
}
}
الف) به روز رسانی اطلاعات Context در صورتیکه از متد context.Database.ExecuteSqlCommand مستقیما استفاده شود
در قسمت قبل با متد context.Database.ExecuteSqlCommand برای اجرای مستقیم عبارات SQL بر روی بانک اطلاعاتی آشنا شدیم. اگر این متد در نیمه کار یک Context فراخوانی شود، به معنای کنار گذاشتن Change Tracker API میباشد؛ زیرا اکنون در سمت بانک اطلاعاتی اتفاقاتی رخ دادهاند که هنوز در Context جاری کلاینت منعکس نشدهاند:
using System;
using System.Data.Entity;
using EF_Sample09.DataLayer.Context;
using EF_Sample09.DomainClasses;
namespace EF_Sample09
{
class Program
{
static void Main(string[] args)
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample09Context, Configuration>());
using (var db = new Sample09Context())
{
var payee = new Payee { Name = "فروشگاه سر کوچه" };
var bill = new Bill { Amount = 4900, Description = "یک سطل ماست", Payee = payee };
db.Bills.Add(bill);
db.SaveChanges();
}
using (var db = new Sample09Context())
{
var bill1 = db.Bills.Find(1);
bill1.Description = "ماست";
db.Database.ExecuteSqlCommand("Update Bills set Description=N'سطل ماست' where id=1");
Console.WriteLine(bill1.Description);
db.Entry(bill1).Reload(); //Refreshing an Entity from the Database
Console.WriteLine(bill1.Description);
db.SaveChanges();
}
}
}
}
در این مثال ابتدا دو رکورد به بانک اطلاعاتی اضافه میشوند. سپس توسط متد db.Bills.Find، اولین رکورد جدول Bills بازگشت داده میشود. در ادامه، خاصیت توضیحات آن به روز شده و سپس با استفاده از متد db.Database.ExecuteSqlCommand نیز بار دیگر خاصیت توضیحات اولین رکورد به روز خواهد شد.
اکنون اگر مقدار bill1.Description را بررسی کنیم، هنوز دارای مقدار پیش از فراخوانی db.Database.ExecuteSqlCommand میباشد، زیرا تغییرات سمت بانک اطلاعاتی هنوز به Context مورد استفاده منعکس نشده است.
در اینجا برای هماهنگی کلاینت با بانک اطلاعاتی، کافی است متد Reload را بر روی موجودیت مورد نظر فراخوانی کنیم.
ب) یکسان سازی ی و ک اطلاعات رشتهای دریافتی پیش از ذخیره سازی در بانک اطلاعاتی
یکی از الزامات برنامههای فارسی، یکسان سازی ی و ک دریافتی از کاربر است. برای این منظور باید پیش از فراخوانی متد SaveChanges نهایی، مقادیر رشتهای کلیه موجودیتها را یافته و به روز رسانی کرد:
using System;
using System.Data;
using System.Data.Entity;
using System.Linq;
using System.Reflection;
using EF_Sample09.DataLayer.Toolkit;
using EF_Sample09.DomainClasses;
namespace EF_Sample09.DataLayer.Context
{
public class MyDbContextBase : DbContext
{
public void RejectChanges()
{
foreach (var entry in this.ChangeTracker.Entries())
{
switch (entry.State)
{
case EntityState.Modified:
entry.State = EntityState.Unchanged;
break;
case EntityState.Added:
entry.State = EntityState.Detached;
break;
}
}
}
public override int SaveChanges()
{
applyCorrectYeKe();
auditFields();
return base.SaveChanges();
}
private void applyCorrectYeKe()
{
//پیدا کردن موجودیتهای تغییر کرده
var changedEntities = this.ChangeTracker
.Entries()
.Where(x => x.State == EntityState.Added || x.State == EntityState.Modified);
foreach (var item in changedEntities)
{
if (item.Entity == null) continue;
//یافتن خواص قابل تنظیم و رشتهای این موجودیتها
var propertyInfos = item.Entity.GetType().GetProperties(
BindingFlags.Public | BindingFlags.Instance
).Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string));
var pr = new PropertyReflector();
//اعمال یکپارچگی نهایی
foreach (var propertyInfo in propertyInfos)
{
var propName = propertyInfo.Name;
var val = pr.GetValue(item.Entity, propName);
if (val != null)
{
var newVal = val.ToString().Replace("ی", "ی").Replace("ک", "ک");
if (newVal == val.ToString()) continue;
pr.SetValue(item.Entity, propName, newVal);
}
}
}
}
private void auditFields()
{
// var auditUser = User.Identity.Name; // in web apps
var auditDate = DateTime.Now;
foreach (var entry in this.ChangeTracker.Entries<BaseEntity>())
{
// Note: You must add a reference to assembly : System.Data.Entity
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedOn = auditDate;
entry.Entity.ModifiedOn = auditDate;
entry.Entity.CreatedBy = "auditUser";
entry.Entity.ModifiedBy = "auditUser";
break;
case EntityState.Modified:
entry.Entity.ModifiedOn = auditDate;
entry.Entity.ModifiedBy = "auditUser";
break;
}
}
}
}
}
اگر به کلاس Context مثال جاری که در ابتدای بحث معرفی شد دقت کرده باشید به این نحو تعریف شده است (بجای DbContext از MyDbContextBase مشتق شده):
public class Sample09Context : MyDbContextBase
علت هم این است که یک سری کد تکراری را که میتوان در تمام Contextها قرار داد، بهتر است در یک کلاس پایه تعریف کرده و سپس از آن ارث بری کرد.
تعاریف کامل کلاس MyDbContextBase را در کدهای فوق ملاحظه میکنید.
در اینجا کار با تحریف متد SaveChanges شروع میشود. سپس در متد applyCorrectYeKe کلیه موجودیتهای تحت نظر ChangeTracker که تغییر کرده باشند یا به آن اضافه شده باشند، یافت شده و سپس خواص رشتهای آنها جهت یکسانی سازی ی و ک، بررسی میشوند.
ج) سادهتر سازی به روز رسانی فیلدهای بازبینی یک رکورد مانند DateCreated، DateLastUpdated و امثال آن بر اساس وضعیت جاری یک موجودیت
در کلاس MyDbContextBase فوق، کار متد auditFields، مقدار دهی خودکار خواص تکراری تاریخ ایجاد، تاریخ به روز رسانی، شخص ایجاد کننده و شخص تغییر دهنده یک رکورد است. به کمک ChangeTracker میتوان به موجودیتهایی از نوع کلاس پایه BaseEntity دست یافت. در اینجا اگر entry.State آنها مساوی EntityState.Added بود، هر چهار خاصیت یاد شده به روز میشوند. اگر حالت موجودیت جاری، EntityState.Modified بود، تنها خواص مرتبط با تغییرات رکورد به روز خواهند شد.
به این ترتیب دیگر نیازی نیست تا در حین ثبت یا ویرایش اطلاعات برنامه نگران این چهار خاصیت باشیم؛ زیرا به صورت خودکار مقدار دهی خواهند شد.
د) پیاده سازی قابلیت لغو تغییرات در برنامه
علاوه بر اینها در کلاس MyDbContextBase، متد RejectChanges نیز تعریف شده است تا بتوان در صورت نیاز، حالت موجودیتهای تغییر کرده یا اضافه شده را به حالت پیش از عملیات، بازگرداند.