اشتراکها
نظرات مطالب
آشنایی با FileTable در SQL Server 2012 بخش 1
استفاده از FileTable در EF
http://www.eidias.com/blog/2013/12/4/using-sql-filetable-in-entity-framework
پاسخ به بازخوردهای پروژهها
استفاده از اشیاء پیچیده در حالت StronglyTypedList
هر دوی این حالتها کار خواهند کرد:
using PdfRpt.Core.Helper; var data = list.GetSafeStringValueOf("Product.Category.Name"); or var data = list.GetSafeStringValueOf<Entity>(x=>x.Product.Category.Name);
نظرات مطالب
ایجاد یک Repository در پروژه برای دستورات EF
برای مواردی که خروجی یک لیست (تعدادی آیتم) باشد از Property استفاده نمیشود. مثلا برای All باید از Method استفاده کنید. Properties vs. Methods
بهتر است برای خروجی متد هایی مانند All نیز به جای لیست از IEnumerable یا IQueryable استفاده کنید.
متدهای Update و Insert نیز به طور جداگانه تعریف شوند. (قرار است هر متد تنها یک وظیفه داشته باشد)
بهتر است برای خروجی متد هایی مانند All نیز به جای لیست از IEnumerable یا IQueryable استفاده کنید.
متدهای Update و Insert نیز به طور جداگانه تعریف شوند. (قرار است هر متد تنها یک وظیفه داشته باشد)
نظرات مطالب
استفاده از فیلدهای XML در NHibernate
به ASP.NET MVC کوچ کنید. از روی مدل شما فرمهای insert/delete/update به همراه اعتبار سنجی و غیره رو همه رو یکجا با ابزارهای توکاری که داره تولید میکنه. حتی مقادیر وارد شده توسط کاربر رو هم به صورت خودکار به فیلدهای مدل انتساب میده (model binding).
پاسخ به بازخوردهای پروژهها
ثبت رکورد جدید به جای بروزرسانی آن
مشکل از متد Update ایی که گذاشتید نیست. در کنترلری که در تصویر مشخص شده، ابتدا downloadLinkService.RemoveByPostId هست و بعد postService.UpdatePost . یعنی این Insert اضافی بخاطر Remove ایی است که در اکشن متد EditPost فایل PostController.cs ناحیه ادمین صورت گرفته (Controller: Admin.PostController.editpost).
فر ض کنید پروژه بزرگی دارید
که هر قسمت را به یک برنامه نویس میسپارید تا آن قسمت را در پروژه مجزایی طراحی و برنامه نویسی کند. هر برنامه نویس Entityهای خاص خود را در لایههای مربوط به پروژه خود تعریف میکند و از آنها استفاده میکند. حال یکی از برنامه نویسها میخواهد از Entity های پروژه دیگر استفاده کند. در این صورت اگر از دو Context شیءایی را بسازد و آنها را با یکدیگر Join بزند، خطایی مربوط به تعلق داشتن دو Entity به دو Context متفاوت را میگیرد.
و در SampleProject2، مدل ProductType را داریم که هر دو Entity از کلاس Entity ارث بری میکند:
همه پروژهها را در پروژهی SampleProject1.Console، به عنوان رفرنس اضافه میکنیم؛ بجز SampleProject2.Console و Output path همه پروژهها را به یک پوشه مشترک هدایت میکنیم. در ادامه برای بدست آوردن Entityها از کد زیر استفاده میکنیم:
و سپس برای Generate کردن کلاس DbContext از کلاس زیر استفاده میکنیم:
در پروژههای کوچک، کل تیم بر روی ماژولهای مختلف یک پروژه کار میکنند و یک DbContext مشترک دارند. اما راه حل این مشکل در پروژههای بزرگ چیست؟
یکی از راههای پیشنهادی، استفاده از یک کلاس DbContextBase است که همه پروژهها بایستی Context خود را از این کلاس به ارث ببرند که در این صورت باز هم مشکل ساخت چند DbContext وجود خواهد داشت که فقط میتوان از Entityهای موجود در DbContextBase و DbContext پروژه جاری استفاده کرد. اما در شرکتهای بزرگ که پروژههایی مانندERP دارند، روش دیگری استفاده میشود که در ادامه خواهیم دید.
روش مورد استفاده به این صورت است که در زمان اجرا یک DbContext برای همه Entityهای پروژههای مختلف ساخته میشود. اجازه بدهید همراه با مثال، این پروژه را پیش برویم. فرض کنید دو تیم برنامه نویسی داریم که هر کدام بر روی پروژههای مجزای SampleProject1 و SampleProject2 کار میکنند که Entityهای هر کدام در لایههای Common قرار گرفتهاند.
در SampleProject1 مدل Product را داریم:
public partial class Product : Entity { public int Id { get; set; } public string Name { get; set; } public Nullable<byte> ProductTypeId { get; set; } }
public partial class ProductType : Entity { public byte Id { get; set; } public string Name { get; set; } }
List<Assembly> allAssemblies = new List<Assembly>(); string path = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); foreach (string dll in Directory.GetFiles(path, "*.Common.dll")) allAssemblies.Add(Assembly.LoadFile(dll)); var type = typeof(Entity); List<Type> types = allAssemblies .SelectMany(s => s.GetTypes()) .Where(p => type.IsAssignableFrom(p)).ToList(); List<string> entities = new List<string>(); foreach (var item in types) { entities.Add(item.Name); } types.Add(typeof(Entity));
public class ContextGenerator { public void Generate(List<string> entities, params Type[] types) { StringBuilder code = new StringBuilder(); code.AppendLine(@" using System.Data.Entity; using System.Data.Entity.Core.EntityClient; using SampleProject1.Common.Models; using SampleProject1.Common.Models.Mapping; using SampleProject2.Common.Models; using SampleProject2.Common.Models.Mapping; namespace DbContextGenerator { public partial class TestContext : DbContext { static TestContext() { Database.SetInitializer<TestContext>(null); } public TestContext() : base(""Data Source=.;Initial Catalog=Test;Integrated Security=True;MultipleActiveResultSets=True"") { } "); var pluralizeHelper = new PluralizeHelper(); foreach (var entity in entities) { code.AppendLine($@"public DbSet<{entity}> {pluralizeHelper.Pluralize(entity)} {{ get; set; }}"); } code.AppendLine(@"protected override void OnModelCreating(DbModelBuilder modelBuilder)"); code.AppendLine(@"{"); foreach (var entity in entities) { code.AppendLine($@"modelBuilder.Configurations.Add(new {entity}Map());"); } code.AppendLine(@"}"); code.AppendLine(@"}"); code.AppendLine(@"}"); CSharpCodeProvider provider = new CSharpCodeProvider(); CompilerParameters parameters = new CompilerParameters(); parameters.ReferencedAssemblies.Add("System.Drawing.dll"); parameters.ReferencedAssemblies.Add("System.Data.dll"); parameters.ReferencedAssemblies.Add("System.Data.Entity.dll"); parameters.ReferencedAssemblies.Add("System.ComponentModel.dll"); foreach (var type in types) { parameters.ReferencedAssemblies.Add(type.Assembly.Location); } parameters.ReferencedAssemblies.Add(typeof(DbSet).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(DbContext).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(IQueryable).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(IQueryable<>).Assembly.Location); parameters.ReferencedAssemblies.Add(typeof(System.ComponentModel.IListSource).Assembly.Location); parameters.GenerateExecutable = false; parameters.GenerateInMemory = false; parameters.OutputAssembly = "ProjectContext.dll"; CompilerResults results = provider.CompileAssemblyFromSource(parameters, code.ToString()); if (results.Errors.HasErrors) { StringBuilder sb = new StringBuilder(); foreach (CompilerError error in results.Errors) { sb.AppendLine(String.Format("Error ({0}): {1}", error.ErrorNumber, error.ErrorText)); } throw new InvalidOperationException(sb.ToString()); } } }
و نحوه فراخوانی آن:
new ContextGenerator().Generate(entities, types.ToArray()); // generate dbContext
همانطور که مشاهده میکنید، برای تولید کد، از کلاس CSharpCodeProvider استفاده میکنیم که نتیجه اجرای کد بالا، ساخت DLLی به نام ProjectContext.dll است. با مشاهده DLL ساخته شده توسط نرم افزار ILSpy، کد جنریت شده به صورت زیر خواهد بود:
حال برای استفاده از Context تولید شده، به صورت زیر شیءایی را ساخته:
static DbContext _dbContext=null; public static DbContext GetDbContextInstance() { if (_dbContext == null) { string path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); var dllversionAssm = Assembly.LoadFile(path + "\\ProjectContext.dll"); Type type = dllversionAssm.GetType("DbContextGenerator.TestContext"); _dbContext = (DbContext)Activator.CreateInstance(type); } return _dbContext; }
و سپس برای ساخت DbSet از هر Entity به کد زیر نیاز خواهیم داشت:
public static System.Data.Entity.DbSet<T> Get<T>() where T : class { var set = GetDbContextInstance().Set<T>(); return set; }
هم اکنون میتوان رکوردهای Entityها را واکشی کرده و یا آنها را با یکدیگر Join بزنیم:
var products = Get<Product>().ToList(); var productTypes = Get<ProductType>().ToList(); var query = from p in Get<Product>() join pt in Get<ProductType>() on p.ProductTypeId equals pt.Id select new { Id = p.Id, Name = p.Name, ProductType = pt.Name }; var JoinResult = query.ToList();
و نتیجه واکشی ها
بر اساس رفتار پیش فرض در دیتابیس SQL Server، در زمان انجام دادن یک دستور که منجر به ایجاد تغییرات در اطلاعات موجود در جدول میشود (برای مثال دستور Update)، جدول مربوطه به صورت کامل Lock میشود، ولو آن دستور Update، فقط با یکی از رکوردهای آن جدول کار داشته باشد.
داریم
در سیستمهای با تعداد تراکنش بالا و دارای تعداد زیاد کلاینت، این رفتار پیش فرض موجب ایجاد صفی از تراکنشهای در حال انتظار بر روی جداولی میشود که ویرایشهای زیادی بر روی آنها رخ میدهد.
اگر چه که بنظر این مشکل راه حلهای زیادی دارد، لکن آن راه حلی که همیشه موثر عمل میکند استفاده از SQL Server Table Hints است.
SQL Server Table Hints به تمامی آن دستوراتی گفته میشود که هنگام اجرای دستور اصلی (برای مثال Select و یا Update) رفتار پیش فرض SQL Server را بر اساس Hint ارائه شده تغییر میدهند.
لیست کامل این Hintها را میتوانید در اینجا مشاهده کنید.
Hint ای که در اینجا برای ما مفید است، آن است که به SQL Server بگوییم هنگام اجرای دستور Update، به جای Lock کردن کل جدول، فقط رکورد در حال ویرایش را Lock کند، و این باعث میشود تا باقی تراکنش ها، که ای بسا با سایر رکوردهای آن جدول کار داشته باشند متوقف نشوند، که البته این مسئله کمی به افزایش مصرف حافظه میانجامد، لکن مقدار افزایش بسیار ناچیز است.
این Hint که rowlock نام دارد در تراکنشهای با Isolation Level تنظیم شده بر روی Snapshot باید با یک Table Hint دیگر با نام updlock ترکیب شود.
توضیحات مفصلتر این دو Hint در لینک مربوطه آمده است.
بنابر این، بجای دستور
update products set Name = "Test" Where Id = 1
update products with (nolock,updlock) set Name = "Test" where Id = 1
تا اینجا مشکل خاصی وجود ندارد، آنچه که از اینجا به بعد اهمیت دارد این است که در هنگام کار با Entity Framework، اساسا ما نویسنده دستورات Update نیستیم که به آنها Hint اضافه کنیم یا نه، بلکه دستورات SQL بوسیله Entity Framework ایجاد میشوند.
در Entity Framework، مکانیزمی تعبیه شده است با نام Db Command Interceptor که به شما اجازه میدهد دستورات SQL ساخته شده را Log کنید و یا قبل از اجرا تغییر دهید، که برای اضافه نمودن Table Hintها ما از این روش استفاده میکنیم، برای انجام این کار داریم: (توضیحات در ادامه)
public class UpdateRowLockHintDbCommandInterceptor : IDbCommandInterceptor { public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<Int32> interceptionContext) { if (command.CommandType != CommandType.Text) return; // (1) if (!(command is SqlCommand)) return; // (2) SqlCommand sqlCommand = (SqlCommand)command; String commandText = sqlCommand.CommandText; String updateCommandRegularExpression = "(update) "; Boolean isUpdateCommand = Regex.IsMatch(commandText, updateCommandRegularExpression, RegexOptions.IgnoreCase | RegexOptions.Multiline); // You may use better regular expression pattern here. if (isUpdateCommand) { Boolean isSnapshotIsolationTransaction = sqlCommand.Transaction != null && sqlCommand.Transaction.IsolationLevel == IsolationLevel.Snapshot; String tableHintToAdd = isSnapshotIsolationTransaction ? " with (rowlock , updlock) set " : " with (rowlock) set "; commandText = Regex.Replace(commandText, "^(set) ", (match) => { return tableHintToAdd; }, RegexOptions.IgnoreCase | RegexOptions.Multiline); command.CommandText = commandText; } }
این کد در قسمت (1) ابتدا تشخیص میدهد که آیا این یک Command دارای Command Text است یا خیر، برای مثال اگر فراخوانی یک Stored Procedure است، ما با آن کاری نداریم.
در قسمت دوم تشخیص میدهیم که آیا با SQL Server در حال تعامل هستیم، یا برای مثال با Oracle و ...، که ما برای Table Hintها فقط با SQL Server کار داریم.
سپس باید تشخیص دهیم که آیا این یک دستور update است یا خیر ؟ برای این منظور از Regular Expressionها استفاده کرده ایم، که خیلی به بحث آموزش این پست مربوط نیست، به صورت کلی از Regular Expressionها برای یافتن و بررسی و جایگزینی عبارات با قاعده در هنگام کار با رشتهها استفاده میشود.
ممکن است Regular Expression ای که شما مینویسید بسیار بهتر از این نمونه باشد، که در این صورت خوشحال میشوم در قسمت نظرات آنرا قرار دهید.
در نهایت با بررسی Transaction Isolation Level مربوطه که Snapshot است یا خیر، به درج یک یا هر دو Table Hint مربوطه اقدام مینماییم.