اشتراکها
اشتراکها
AutoMapper توکار Entity Framework 6
اشتراکها
معرفی Entity Framework 7
در مطلب قبلی، مدل EAV را معرفی کردیم و گفتیم که این نوع پیادهسازی در واقع یک SQL Smell است؛ زیرا کوئری نویسی را سخت میکند و همچنین به دلیل عدم امکان تعریف constraints، کنترلی بر روی صحت دیتاهای وارده شد نخواهیم داشت. در نهایت با برنامهای روبرو خواهیم شد که درک صحیحی از ماهیت دیتا ندارد. اما اگر در شرایطی مجبور به استفادهی از این مدل هستید، بهتر است از فرمت JSON برای ذخیرهسازی دیتای داینامیک استفاده کنید. بیشتر دیتابیسهای رابطهایی به صورت native از نوع دادهایی JSON پشتیبانی میکنند:
CREATE TABLE EmployeeJsonAttributes ( Id int NOT NULL AUTO_INCREMENT, EmployeeId int NOT NULL, Attributes json DEFAULT NULL, PRIMARY KEY (Id), FOREIGN KEY (EmployeeId) REFERENCES EmployeeEav (Id) ON DELETE CASCADE )
INSERT INTO EmployeeJsonAttributes VALUES ( 101, '{ "name": "Jon", "lastName": "Doe", "dateOfBirth": "1989-01-01 10:10:10+05:30", "skills": [ "C#", "JS" ], "address": { "country": "UK", "city": "London", "email": "jon.doe@example.com" } }' ) INSERT INTO efcoresample.EmployeeJsonAttributes VALUES ( 101, JSON_OBJECT( "name", "Jon", "lastName", "Doe", "dateOfBirth", "1989-01-01 10:10:10+05:30", "skills", JSON_ARRAY("C#", "JS"), "address", JSON_OBJECT( "country", "UK", "city", "London", "email", "jon.doe@example.com" ) ) )
به عنوان مثال در ادامه میخواهیم کشور محل تولد یک کاربر خاص را نمایش دهیم. برای اینکار میتوانیم از JSON_EXTRACT استفاده کنیم:
SELECT JSON_EXTRACT(Attributes, '$.address.country') as Country FROM EmployeeJsonAttributes WHERE EmployeeId = 101; -- Conutry -- "UK"
همچنین میتوانیم از عملگر column-path نیز به جای JSON_EXTRACT استفاده کنیم:
SELECT Attributes -> '$.address.country' as Country FROM EmployeeJsonAttributes WHERE EmployeeId = 101; -- Conutry -- "UK"
بنابراین به راحتی میتوانیم کوئری مطلب قبل را اینگونه بازنویسی کنیم:
SELECT EmployeeId, Attributes ->> '$.DateOfBirth' AS BirthDate FROM EmployeeJsonAttributes WHERE Attributes ->> '$.DateOfBirth' > DATE_SUB(CURRENT_DATE(), INTERVAL 25 YEAR)
استفاده از JSON در EF Core
متاسفانه در EF Core به صورت مستقیم نمیتوانیم از JSON درون کلاسهای سیشارپ استفاده کنیم (+ )، در نتیجه در سمت کلاسهای سیشارپ باید از string استفاده کنیم و به نوعی به EF Core اطلاع دهیم که تایپ ستون موردنظرمان JSON است. در نتیجه خروجی نهایی درون دیتابیس، یک فیلد با تایپ JSON خواهد بود. برای اینکار به دو شیوه میتوانیم تایپ ستون موردنظر را تعیین کنیم:
// Fluent API protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Employee>(entity => { entity.Property(e => e.Attributes).HasColumnType("json"); }); } // Data Annotations [Column(TypeName = "json")] public string Attributes { get; set; }
در نهایت برای تشکیل بانک اطلاعاتی، به مدلی با ساختار زیر نیاز خواهیم داشت:
public class EmployeeJsonAttribute { public int Id { get; set; } public virtual EmployeeEav Employee { get; set; } public int EmployeeId { get; set; } [Column(TypeName = "json")] public string Attributes { get; set; } }
dbContext.EmployeeJsonAttributes.Add(new EmployeeJsonAttribute { EmployeeId = 101, Attributes = JsonSerializer.Serialize(new { FirstName = "Sirwan", LastName = "Afifi", DateOfBirth = DateTime.Now.AddYears(-31) }) }); dbContext.SaveChanges();
var employee = dbContext.EmployeeJsonAttributes.Find(201); Console.WriteLine(JsonSerializer.Deserialize<Employee>(employee.Attributes).DateOfBirth);
برای نوشتن کوئری روی ستون JSON میتوانید از Query Types نیز استفاده کنید.
مدتی است که حالت READ_COMMITTED_SNAPSHOT بسیار مورد توجه واقع شده:
- در سایت Stack overflow از آن استفاده میشود (^).
- در SQL Server Azure حالت پیش فرض ایجاد دیتابیسها و تراکنشهای جدید است (^).
- در Entity framework 6 حالت پیش فرض تراکنشهای ایجاد شده، قرار گرفته است (^ ).
و ... در Oracle، تنها حالت مدیریت مسایل همزمانی است! (البته به نام MVCC، اما با همین عملکرد)
اما READ_COMMITTED_SNAPSHOT در SQL Server چیست و کاربرد آن کجا است؟
اگر استفاده گسترده و سنگینی از SQL Server داشته باشید، حتما به پیغامهای خطای deadlock آن برخوردهاید:
روش پیش فرض مدیریت مسایل همزمانی در SQL Server، حالت READ COMMITTED است. به این معنا که اگر در طی یک تراکنش مشغول به تغییر اطلاعاتی باشیم، سایر کاربران از خواندن نتیجه آن (اصطلاحا به آن Dirty read گفته میشود) منع خواهند شد؛ تا زمانیکه این تراکنش با موفقیت به پایان برسد. هرچند در این حالت سایر تراکنشها امکان ویرایش یا حذف اطلاعات را خواهند داشت. به علاوه اگر در طی این تراکنش، اطلاعاتی خوانده شوند، سایر تراکنشها تا پایان تراکنش جاری، قادر به تغییر این اطلاعات خوانده شده نخواهند بود (منشاء بروز خطاهای deadlock یاد شده در سیستمهای پرترافیک).
در SQL Server 2005 برای بهبود مقیاس پذیری SQL Server و کاهش خطاهای deadlock، مکانیزم READ_COMMITTED_SNAPSHOT معرفی گشت.
به صورت خلاصه زمانیکه که تراکنش مورد نظر تحت حالت READ COMMITTED SNAPSHOT انجام میشود، optimistic reads and pessimistic writes خواهیم داشت (خواندنهای خوشبینانه و نوشتنهای بدبینانه). در این حالت تضمین میشود که خواندن اطلاعات داخل یک تراکنش، شامل اطلاعات تغییر یافته توسط سایر تراکنشهای همزمان نخواهد بود. همچنین زمانیکه در این بین، اطلاعاتی خوانده میشود، بر روی این اطلاعات برخلاف حالت READ COMMITTED قفل قرار داده نمیشود. بنابراین تراکنشهایی که درحال خواندن اطلاعات هستند، تراکنشهای همزمانی را که در حال نوشتن اطلاعات میباشند، قفل نخواهد کرد و برعکس.
نحوه فعال سازی READ_COMMITTED_SNAPSHOT
فعال سازی READ_COMMITTED_SNAPSHOT باید ابتدا در سطح یک بانک اطلاعاتی SQL Server انجام شود:
کاری که در اینجا انجام خواهد شد، ایجاد یک snapshot یا یک کپی فقط خواندنی، از بانک اطلاعاتی کاری شما میباشد. بنابراین در این حالت، زمانیکه یک عبارت Select را فراخوانی میکنید، این خواندن، از بانک اطلاعاتی فقط خواندنی تشکیل شده، صورت خواهد گرفت. اما تغییرات بر روی دیتابیس اصلی کاری درج شده و سپس این snapshot به روز میشود.
حالت READ_COMMITTED_SNAPSHOT خصوصا برای برنامههای وبی که تعداد بالایی Read در مقابل تعداد کمی Write دارند، به شدت بر روی کارآیی و بالا رفتن سرعت و مقیاس پذیری آنها تاثیر خواهد داشت؛ به همراه حداقل تعداد deadlockهای حاصل شده.
در Entity framework وضعیت به چه صورتی است؟
EF از حالت پیش فرض مدیریت مسایل همزمانی در SQL Server یا همان حالت READ COMMITTED در زمان فراخوانی متد SaveChanges استفاده میکند.
در EF 6 این حالت پیش فرض به READ_COMMITTED_SNAPSHOT تغییر کرده است. البته همانطور که عنوان شد، پیشتر باید بانک اطلاعاتی را نیز جهت پذیرش این نوع تراکنشها آماده ساخت.
اگر از نگارشهای پایینتر از EF 6 استفاده میکنید، برای استفاده از حالت READ_COMMITTED_SNAPSHOT باید صراحتا IsolationLevel را مشخص ساخت:
- در سایت Stack overflow از آن استفاده میشود (^).
- در SQL Server Azure حالت پیش فرض ایجاد دیتابیسها و تراکنشهای جدید است (^).
- در Entity framework 6 حالت پیش فرض تراکنشهای ایجاد شده، قرار گرفته است (^ ).
و ... در Oracle، تنها حالت مدیریت مسایل همزمانی است! (البته به نام MVCC، اما با همین عملکرد)
اما READ_COMMITTED_SNAPSHOT در SQL Server چیست و کاربرد آن کجا است؟
اگر استفاده گسترده و سنگینی از SQL Server داشته باشید، حتما به پیغامهای خطای deadlock آن برخوردهاید:
Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
در SQL Server 2005 برای بهبود مقیاس پذیری SQL Server و کاهش خطاهای deadlock، مکانیزم READ_COMMITTED_SNAPSHOT معرفی گشت.
به صورت خلاصه زمانیکه که تراکنش مورد نظر تحت حالت READ COMMITTED SNAPSHOT انجام میشود، optimistic reads and pessimistic writes خواهیم داشت (خواندنهای خوشبینانه و نوشتنهای بدبینانه). در این حالت تضمین میشود که خواندن اطلاعات داخل یک تراکنش، شامل اطلاعات تغییر یافته توسط سایر تراکنشهای همزمان نخواهد بود. همچنین زمانیکه در این بین، اطلاعاتی خوانده میشود، بر روی این اطلاعات برخلاف حالت READ COMMITTED قفل قرار داده نمیشود. بنابراین تراکنشهایی که درحال خواندن اطلاعات هستند، تراکنشهای همزمانی را که در حال نوشتن اطلاعات میباشند، قفل نخواهد کرد و برعکس.
نحوه فعال سازی READ_COMMITTED_SNAPSHOT
فعال سازی READ_COMMITTED_SNAPSHOT باید ابتدا در سطح یک بانک اطلاعاتی SQL Server انجام شود:
ALTER DATABASE testDatabase SET ALLOW_SNAPSHOT_ISOLATION ON; ALTER DATABASE testDatabase SET READ_COMMITTED_SNAPSHOT ON;
حالت READ_COMMITTED_SNAPSHOT خصوصا برای برنامههای وبی که تعداد بالایی Read در مقابل تعداد کمی Write دارند، به شدت بر روی کارآیی و بالا رفتن سرعت و مقیاس پذیری آنها تاثیر خواهد داشت؛ به همراه حداقل تعداد deadlockهای حاصل شده.
در Entity framework وضعیت به چه صورتی است؟
EF از حالت پیش فرض مدیریت مسایل همزمانی در SQL Server یا همان حالت READ COMMITTED در زمان فراخوانی متد SaveChanges استفاده میکند.
در EF 6 این حالت پیش فرض به READ_COMMITTED_SNAPSHOT تغییر کرده است. البته همانطور که عنوان شد، پیشتر باید بانک اطلاعاتی را نیز جهت پذیرش این نوع تراکنشها آماده ساخت.
اگر از نگارشهای پایینتر از EF 6 استفاده میکنید، برای استفاده از حالت READ_COMMITTED_SNAPSHOT باید صراحتا IsolationLevel را مشخص ساخت:
using (var transactionScope = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions { IsolationLevel= IsolationLevel.Snapshot })) { // update some tables using entity framework context.SaveChanges(); transactionScope.Complete(); }
Lazy را به قسمتی از درون Func اعمال کردید. کل Func باید Lazy شود تا درست کار کند.
using System; using System.Collections.Concurrent; using System.Threading; namespace LazyDic { public class LazyConcurrentDictionary<TKey, TValue> { private readonly ConcurrentDictionary<TKey, Lazy<TValue>> _concurrentDictionary; public LazyConcurrentDictionary() { _concurrentDictionary = new ConcurrentDictionary<TKey, Lazy<TValue>>(); } public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) { var lazyResult = _concurrentDictionary.GetOrAdd(key, k => new Lazy<TValue>(() => valueFactory(k), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value; } public TValue AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory) { var lazyResult = _concurrentDictionary.AddOrUpdate( key, new Lazy<TValue>(() => addValue), (k, currentValue) => new Lazy<TValue>(() => updateValueFactory(k, currentValue.Value), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value; } public TValue AddOrUpdate(TKey key, Func<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory) { var lazyResult = _concurrentDictionary.AddOrUpdate( key, k => new Lazy<TValue>(() => addValueFactory(k)), (k, currentValue) => new Lazy<TValue>(() => updateValueFactory(k, currentValue.Value), LazyThreadSafetyMode.ExecutionAndPublication)); return lazyResult.Value; } public int Count => _concurrentDictionary.Count; } }
تا قبل از EF 6 برای تهیه لاگ SQL تولیدی توسط Entity framework نیاز بود به ابزارهای ثالث متوسل شد. برای مثال از انواع پروفایلرها استفاده کرد (^ و ^ و ^). اما در EF 6 امکان توکاری به نام Command Interception تدارک دیده شده است تا توسط آن بتوان بدون نیاز به ابزارهای جانبی، به درون سیستم EF متصل شد و دستورات تولیدی آنرا پیش از اجرای بر روی بانک اطلاعاتی دریافت و مثلا لاگ کرد. در ادامه نمونهای از این عملیات را بررسی خواهیم کرد.
تهیه کلاس SimpleInterceptor
برای اتصال به متدهای اجرای دستورات SQL در EF 6 تنها کافی است یک کلاس جدید را از کلاس پایه DbCommandInterceptor مشتق کرده و سپس متدهای کلاس پایه را override کنیم. در این متدها، فراخوانی متدهای کلاس پایه، معادل خواهند بود با اجرای واقعی دستور بر روی بانک اطلاعاتی. به این ترتیب حتی میتوان مدت زمان انجام عملیات را نیز بدست آورد. در اینجا command.CommandText معادل است با دستور SQL در حال اجرا و همچنین نیاز است تا تمام سطوح تو در توی استثناهای احتمالی رخ داده را نیز بررسی کرد:
نحوه استفاده از کلاس SimpleInterceptor
کلاس فوق را کافی است تنها یکبار در آغاز برنامه (مثلا در متد Application_Start برنامههای وب) به EF 6 معرفی کرد:
اکنون اگر برنامه را اجرا کنیم، خروجی SQL و زمانهای اجرای عملیات را در پنجره دیباگ VS.NET میتوان مشاهده کرد:
تهیه کلاس SimpleInterceptor
برای اتصال به متدهای اجرای دستورات SQL در EF 6 تنها کافی است یک کلاس جدید را از کلاس پایه DbCommandInterceptor مشتق کرده و سپس متدهای کلاس پایه را override کنیم. در این متدها، فراخوانی متدهای کلاس پایه، معادل خواهند بود با اجرای واقعی دستور بر روی بانک اطلاعاتی. به این ترتیب حتی میتوان مدت زمان انجام عملیات را نیز بدست آورد. در اینجا command.CommandText معادل است با دستور SQL در حال اجرا و همچنین نیاز است تا تمام سطوح تو در توی استثناهای احتمالی رخ داده را نیز بررسی کرد:
using System; using System.Data.Common; using System.Data.Entity.Infrastructure.Interception; using System.Diagnostics; using System.Text; namespace EFCommandInterception { public class SimpleInterceptor : DbCommandInterceptor { public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { var timespan = runCommand(() => base.ScalarExecuting(command, interceptionContext)); logData(command, interceptionContext.Exception, timespan); } public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { var timespan = runCommand(() => base.NonQueryExecuting(command, interceptionContext)); logData(command, interceptionContext.Exception, timespan); } public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { var timespan = runCommand(() => base.ReaderExecuting(command, interceptionContext)); logData(command, interceptionContext.Exception, timespan); } private static Stopwatch runCommand(Action command) { var timespan = Stopwatch.StartNew(); command(); timespan.Stop(); return timespan; } private static void logData(DbCommand command, Exception exception, Stopwatch timespan) { if (exception != null) { Trace.TraceError(formatException(exception, "Error executing command: {0}", command.CommandText)); } else { Trace.TraceInformation(string.Concat("Elapsed time: ", timespan.Elapsed, " Command: ", command.CommandText)); } } private static string formatException(Exception exception, string fmt, params object[] vars) { var sb = new StringBuilder(); sb.Append(string.Format(fmt, vars)); sb.Append(" Exception: "); sb.Append(exception.ToString()); while (exception.InnerException != null) { sb.Append(" Inner exception: "); sb.Append(exception.InnerException.ToString()); exception = exception.InnerException; } return sb.ToString(); } } }
کلاس فوق را کافی است تنها یکبار در آغاز برنامه (مثلا در متد Application_Start برنامههای وب) به EF 6 معرفی کرد:
DbInterception.Add(new SimpleInterceptor());
پیشنهادها
بررسی GraphDiff
به روز رسانی گرافی از اشیاء (مثلا یک رابطه one-to-many یا many-to-many) منقطع از Context در EF کار زمانبری است؛ زیرا EF نمیداند که چه اشیایی در این بین تغییر کردهاند، کدامها حذف شدهاند و کدامها جدید هستند. کتابخانهی GraphDiff راهحلی را برای این مشکل ارائه داده است:
Introducing GraphDiff for Entity Framework Code First
GraphDiff Aggregate Mappings
GraphDiff v1.3.5
Updates to GraphDiff with new scenarios supported
How to update an expression tree with GraphDiff
Update Many-to-Many Association with GraphDiff
Introducing GraphDiff for Entity Framework Code First
GraphDiff Aggregate Mappings
GraphDiff v1.3.5
Updates to GraphDiff with new scenarios supported
How to update an expression tree with GraphDiff
Update Many-to-Many Association with GraphDiff
نظرات مطالب
EF Code First #3
- ایجاد کلید منحصربفرد ترکیبی روی چند ستون
+ یک سری از فضاهای نام از EF 4 به EF 6 اندکی تغییر کردهاند که در مطلب «ارتقاء به Entity framework 6 » به آن اشاره شدهاست. در کل باید اجازه دهید تا NuGet ارجاعات قدیمی را به صورت خودکار حذف کند و ارجاعات جدید را اضافه کند. بعد هم از فضای نام جدید بدون مشکل میتوانید استفاده کنید.
+ اگر از دات نت 4 استفاده میکردید و اکنون برنامه را به دات نت 4.5 ارتقاء دادید، باید این دستور را صادر کنید:
به این ترتیب از EF مخصوص دات نت 4.5 استفاده خواهد شد. در غیر اینصورت تداخل فضای نام پیدا میکنید.
+ یک سری از فضاهای نام از EF 4 به EF 6 اندکی تغییر کردهاند که در مطلب «ارتقاء به Entity framework 6 » به آن اشاره شدهاست. در کل باید اجازه دهید تا NuGet ارجاعات قدیمی را به صورت خودکار حذف کند و ارجاعات جدید را اضافه کند. بعد هم از فضای نام جدید بدون مشکل میتوانید استفاده کنید.
PM> update-package
PM> update-package -reinstall