در ادامه بحث ASP.NET MVC ميشود به ابزاري به نام MVC Scaffolding اشاره كرد. كار اين ابزار كه توسط يكي از اعضاي تيم ASP.NET MVC به نام استيو اندرسون تهيه شده، توليد كدهاي اوليه يك برنامه كامل ASP.NET MVC از روي مدلهاي شما ميباشد. حجم بالايي از كدهاي تكراري آغازين برنامه را ميشود توسط اين ابزار توليد و بعد سفارشي كرد. MVC Scaffolding حتي قابليت توليد كد بر اساس الگوي Repository و يا نوشتن Unit tests مرتبط را نيز دارد. بديهي است اين ابزار جاي يك برنامه نويس را نميتواند پر كند اما كدهاي آغازين يك سري كارهاي متداول و تكراري را به خوبي ميتواند پياده سازي و ارائه دهد. زير ساخت اين ابزار، علاوه بر ASP.NET MVC، آشنايي با Entity framework code first است.
در طي سري ASP.NET MVC كه در اين سايت تا به اينجا مطالعه كرديد من به شدت سعي كردم از ابزارگرايي پرهيز كنم. چون شخصي كه نميداند مسيريابي چيست، اطلاعات چگونه به يك كنترلر منتقل يا به يك View ارسال ميشوند، قراردادهاي پيش فرض فريم ورك چيست يا زير ساخت امنيتي يا فيلترهاي ASP.NET MVC كدامند، چطور ميتواند از ابزار پيشرفته Code generator ايي استفاده كند، يا حتي در ادامه كدهاي توليدي آنرا سفارشي سازي كند؟ بنابراين براي استفاده از اين ابزار و درك كدهاي توليدي آن، نياز به يك پيشنياز ديگر هم وجود دارد: «Entity framework code first»
امسال دو كتاب خوب در اين زمينه منتشر شدهاند به نامهاي:
Programming Entity Framework: DbContext, ISBN: 978-1-449-31296-1
Programming Entity Framework: Code First, ISBN: 978-1-449-31294-7
كه هر دو به صورت اختصاصي به مقوله EF Code first پرداختهاند.
در طي روزهاي بعدي EF Code first را با هم مرور خواهيم كرد و البته اين مرور مستقل است از نوع فناوري ميزبان آن؛ ميخواهد يك برنامه كنسول باشد يا WPF يا يك سرويس ويندوز NT و يا ... يك برنامه وب.
البته از ديدگاه مايكروسافت، M در MVC به معناي EF Code first است. به همين جهت MVC3 به صورت پيش فرض ارجاعي را به اسمبليهاي آن دارد و يا حتي به روز رساني كه براي آن ارائه داده نيز در جهت تكميل همين بحث است.
مروري سريع بر تاريخچه Entity framework code first
ويژوال استوديو 2010 و دات نت 4، به همراه EF 4.0 ارائه شدند. با اين نگارش امكان استفاده از حالتهاي طراحي database first و model first مهيا است. پس از آن، به روز رسانيهاي EF خارج از نوبت و به صورت منظم، هر از چندگاهي ارائه ميشوند و در زمان نگارش اين مطلب، آخرين نگارش پايدار در دسترس آن 4.3.1 ميباشد. از زمان EF 4.1 به بعد، نوع جديدي از مدل سازي به نام Code first به اين فريم ورك اضافه شد و در نگارشهاي بعدي آن، مباحث DB migration جهت ساده سازي تطابق اطلاعات مدلها با بانك اطلاعاتي، اضافه گرديدند. در روش Code first، كار با طراحي كلاسها كه در اينجا مدل دادهها ناميده ميشوند، شروع گرديده و سپس بر اساس اين اطلاعات، توليد يك بانك اطلاعاتي جديد و يا استفاده از نمونهاي موجود ميسر ميگردد.
پيشتر در روش database first ابتدا يك بانك اطلاعاتي موجود، مهندسي معكوس ميشد و از روي آن فايل XML ايي با پسوند EDMX توليد ميگشت. سپس به كمك entity data model designer ويژوال استوديو، اين فايل نمايش داده شده و يا امكان اعمال تغييرات بر روي آن ميسر ميشد. همچنين در روش ديگري به نام model first نيز كار از entity data model designer جهت طراحي موجوديتها آغاز ميگشت.
اما با روش Code first ديگر در ابتداي امر مدل فيزيكي و يك بانك اطلاعاتي وجود خارجي ندارد. در اينجا EF تعاريف كلاسهاي شما را بررسي كرده و بر اساس آن، اطلاعات نگاشتهاي خواص كلاسها به جداول و فيلدهاي بانك اطلاعاتي را تشكيل ميدهد. البته عموما تعاريف ساده كلاسها بر اين منظور كافي نيستند. به همين جهت از يك سري متاديتا به نام ويژگيها يا اصطلاحا data annotations مهيا در فضاي نام System.ComponentModel.DataAnnotations براي افزودن اطلاعات لازم مانند نام فيلدها، جداول و يا تعاريف روابط ويژه نيز استفاده ميگردد. به علاوه در روش Code first يك API جديد به نام Fluent API نيز جهت تعاريف اين ويژگيها و روابط، با كدنويسي مستقيم نيز درنظر گرفته شده است. نهايتا از اين اطلاعات جهت نگاشت كلاسها به بانك اطلاعاتي و يا براي توليد ساختار يك بانك اطلاعاتي خالي جديد نيز ميتوان كمك گرفت.
مزاياي EF Code first
- مطلوب برنامه نويسها! : برنامه نويسهايي كه مدتي تجربه كار با ابزارهاي طراح را داشته باشند به خوبي ميدانند اين نوع ابزارها عموما demo-ware هستند. چندجا كليك ميكنيد، دوبار Next، سه بار OK و ... به نظر ميرسد كار تمام شده. اما واقعيت اين است كه عمري را بايد صرف نگهداري و يا پياده سازي جزئياتي كرد كه انجام آنها با كدنويسي مستقيم بسيار سريعتر، سادهتر و با كنترل بيشتري قابل انجام است.
- سرعت: براي كار با EF Code first نيازي نيست در ابتداي كار بانك اطلاعاتي خاصي وجود داشته باشد. كلاسهاي خود را طراحي و شروع به كدنويسي كنيد.
- سادگي: در اينجا ديگر از فايلهاي EDMX خبري نيست و نيازي نيست مرتبا آنها را به روز كرده يا نگهداري كرد. تمام كارها را با كدنويسي و كنترل بيشتري ميتوان انجام داد. به علاوه كنترل كاملي بر روي كد نهايي تهيه شده نيز وجود دارد و توسط ابزارهاي توليد كد، ايجاد نميشوند.
- طراحي بهتر بانك اطلاعاتي نهايي: اگر طرح دقيقي از مدلهاي برنامه داشته باشيم، ميتوان آنها را به المانهاي كوچك و مشخصي، تقسيم و refactor كرد. همين مساله در نهايت مباحث database normalization را به نحوي مطلوب و با سرعت بيشتري ميسر ميكند.
- امكان استفاده مجدد از طراحي كلاسهاي انجام شده در ساير ORMهاي ديگر. چون طراحي مدلهاي برنامه به بانك اطلاعاتي خاصي گره نميخورند و همچنين الزاما هم قرار نيست جزئيات كاري EF در آنها لحاظ شود، اين كلاسها در صورت نياز در ساير پروژهها نيز به سادگي قابل استفاده هستند.
- رديابي سادهتر تغييرات: روش اصولي كار با پروژههاي نرم افزاري همواره شامل استفاده از يك ابزار سورس كنترل مانند SVN، Git، مركوريال و امثال آن است. به اين ترتيب رديابي تغييرات انجام شده به سادگي توسط اين ابزارها ميسر ميشوند.
- سادهتر شدن طراحيهاي پيچيدهتر: براي مثال پياده سازي ارث بري، ايجاد كلاسهاي خود ارجاع دهنده و امثال آن با كدنويسي سادهتر است.
دريافت آخرين نگارش EF
براي دريافت و نصب آخرين نگارش EF نياز است از NuGet استفاده شود و اين مزايا را به همراه دارد:
به كمك NuGet امكان با خبر شدن از به روز رساني جديد صورت گرفته به صورت خودكار درنظر گرفته شده است و همچنين كار دريافت بستههاي مرتبط و به روز رساني ارجاعات نيز در اين حالت خودكار است. به علاوه توسط NuGet امكان دسترسي به كتابخانههايي كه مثلا در گوگلكد قرار دارند و به صورت معمول امكان دريافت آنها براي ما ميسر نيست، نيز بدون مشكل فراهم است (براي نمونه ELMAH، كه اصل آن از گوگلكد قابل دريافت است؛ اما بسته نيوگت آن نيز در دسترس ميباشد).
پس از نصب NuGet، تنها كافي است بر روي گره References در Solution explorer ويژوال استوديو، كليك راست كرده و به كمك NuGet آخرين نگارش EF را نصب كرد. در گالري آنلاين آن، عموما EF اولين گزينه است (به علت تعداد بالاي دريافت آن).
حين استفاده از NuGet جهت نصب Ef، ابتدا ارجاعاتي به اسمبليهاي زير به برنامه اضافه خواهند شد:
System.ComponentModel.DataAnnotations.dll
System.Data.Entity.dll
EntityFramework.dll
بديهي است بدون استفاده از NuGet، تمام اين كارها را بايد دستي انجام داد.
سپس در پوشهاي به نام packages، فايلهاي مرتبط با EF قرار خواهند گرفت كه شامل اسمبلي آن به همراه ابزارهاي DB Migration است. همچنين فايل packages.config كه شامل تعاريف اسمبليهاي نصب شده است به پروژه اضافه ميشود. NuGet به كمك اين فايل و شماره نگارش درج شده در آن، شما را از به روز رسانيهاي بعدي مطلع خواهد ساخت:
<?xml version="1.0" encoding="utf-8"?> <packages> <package id="EntityFramework" version="4.3.1" /> </packages>
همچنين اگر به فايل app.config يا web.config برنامه نيز مراجعه كنيد، يك سري تنظيمات ابتدايي اتصال به بانك اطلاعاتي در آن ذكر شده است:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> <!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 --> <section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=4.3.1.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> </configSections> <entityFramework> <defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework"> <parameters> <parameter value="Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=True" /> </parameters> </defaultConnectionFactory> </entityFramework> </configuration>
همانطور كه ملاحظه ميكنيد بانك اطلاعاتي پيش فرضي كه در اينجا ذكر شده است، LocalDB ميباشد. اين بانك اطلاعاتي را از اين آدرس نيز ميتوانيد دريافت كنيد.
البته ضرورتي هم به استفاده از آن نيست و از ساير نگارشهاي SQL Server نيز ميتوان استفاده كرد ولي خوب ... مزيت استفاده از آن براي كاربر نهايي اين است كه «نيازي به يك مهندس براي نصب، راه اندازي و نگهداري ندارد». تنها مشكل آن اين است كه از ويندوز XP پشتيباني نميكند. البته SQL Server CE 4.0 اين محدوديت را ندارد.
ضمن اينكه بايد درنظر داشت EF به فناوري ميزبان خاصي گره نخورده است و مثالهايي كه در اينجا بررسي ميشوند صرفا تعدادي برنامه كنسول معمولي هستند و نكات عنوان شده در آنها در تمام فناوريهاي ميزبان موجود به يك نحو كاربرد دارند.
قراردادهاي پيش فرض EF Code first
عنوان شد كه اطلاعات كلاسهاي ساده تشكيل دهنده مدلهاي برنامه، براي تعريف جداول و فيلدهاي يك بانك اطلاعات و همچنين مشخص سازي روابط بين آنها كافي نيستند و مرسوم است براي پر كردن اين خلاء از يك سري متاديتا و يا Fluent API مهيا نيز استفاده گردد. اما در EF Code first يك سري قرار داد توكار نيز وجود دارند كه مطلع بودن از آنها سبب خواهد شد تا حجم كدنويسي و تنظيمات جانبي اين فريم ورك به حداقل برسند. براي نمونه مدلهاي معروف بلاگ و مطالب آنرا درنظر بگيريد:
namespace EF_Sample01.Models { public class Post { public int Id { set; get; } public string Title { set; get; } public string Content { set; get; } public virtual Blog Blog { set; get; } } }
using System.Collections.Generic; namespace EF_Sample01.Models { public class Blog { public int Id { set; get; } public string Title { set; get; } public string AuthorName { set; get; } public IList<Post> Posts { set; get; } } }
يكي از قراردادهاي EF Code first اين است كه كلاسهاي مدل شما را جهت يافتن خاصيتي به نام Id يا ClassId مانند BlogId، جستجو ميكند و از آن به عنوان primary key و فيلد identity جدول بانك اطلاعاتي استفاده خواهد كرد.
همچنين در كلاس Blog، خاصيت ليستي از Posts و در كلاس Post خاصيت virtual ايي به نام Blog وجود دارند. به اين ترتيب روابط بين دو كلاس و ايجاد كليد خارجي متناظر با آنرا به صورت خودكار انجام خواهد داد.
نهايتا از اين اطلاعات جهت تشكيل database schema يا ساختار بانك اطلاعاتي استفاده ميگردد.
اگر به فضاهاي نام دو كلاس فوق دقت كرده باشيد، به كلمه Models ختم شدهاند. به اين معنا كه در پوشهاي به همين نام در پروژه جاري قرار دارند. يا مرسوم است كلاسهاي مدل را در يك پروژه class library مجزا به نام DomainClasses نيز قرار دهند. اين پروژه نيازي به ارجاعات اسمبليهاي EF ندارد و تنها به اسمبلي System.ComponentModel.DataAnnotations.dll نياز خواهد داشت.
EF Code first چگونه كلاسهاي مورد نظر را انتخاب ميكند؟
ممكن است دهها و صدها كلاس در يك پروژه وجود داشته باشند. EF Code first چگونه از بين اين كلاسها تشخيص خواهد داد كه بايد از كداميك استفاده كند؟ اينجا است كه مفهوم جديدي به نام DbContext معرفي شده است. براي تعريف آن يك كلاس ديگر را به پروژه براي مثال به نام Context اضافه كنيد. همچنين مرسوم است كه اين كلاس را در پروژه class library ديگري به نام DataLayer اضافه ميكنند. اين پروژه نياز به ارجاعي به اسمبليهاي EF خواهد داشت. در ادامه كلاس جديد اضافه شده بايد از كلاس DbContext مشتق شود:
using System.Data.Entity; using EF_Sample01.Models; namespace EF_Sample01 { public class Context : DbContext { public DbSet<Blog> Blogs { set; get; } public DbSet<Post> Posts { set; get; } } }
سپس در اينجا به كمك نوع جنريكي به نام DbSet، كلاسهاي دومين برنامه را معرفي ميكنيم. به اين ترتيب، EF Code first ابتدا به دنبال كلاسي مشتق شده از DbContext خواهد گشت. پس از يافتن آن، خواصي از نوع DbSet را بررسي كرده و نوعهاي متناظر با آنرا به عنوان كلاسهاي دومين درنظر ميگيرد و از ساير كلاسهاي برنامه صرفنظر خواهد كرد. اين كل كاري است كه بايد انجام شود.
اگر دقت كرده باشيد، نام كلاسهاي موجوديتها، مفرد هستند و نام خواص تعريف شده به كمك DbSet، جمع ميباشند كه نهايتا متناظر خواهند بود با نام جداول بانك اطلاعاتي تشكيل شده.
تشكيل خودكار بانك اطلاعاتي و افزودن اطلاعات به جداول
تا اينجا بدون تهيه يك بانك اطلاعاتي نيز ميتوان از كلاس Context تهيه شده استفاده كرد و كار كدنويسي را آغاز نمود. بديهي است جهت اجراي نهايي كدها، نياز به يك بانك اطلاعاتي خواهد بود. اگر تنظيمات پيش فرض فايل كانفيگ برنامه را تغيير ندهيم، از همان defaultConnectionFactory ياده شده استفاده خواهد كرد. در اين حالت نام بانك اطلاعاتي به صورت خودكار تنظيم شده و مساوي «EF_Sample01.Context» خواهد بود.
براي سفارشي سازي آن نياز است فايل app.config يا web.config برنامه را اندكي ويرايش نمود:
<?xml version="1.0" encoding="utf-8"?> <configuration> <configSections> ... </configSections> <connectionStrings> <clear/> <add name="Context" connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true" providerName="System.Data.SqlClient" /> </connectionStrings> ... </configuration>
در اينجا به بانك اطلاعاتي testdb2012 در وهله پيش فرض SQL Server نصب شده، اشاره شده است. فقط بايد دقت داشت كه تگ configSections بايد در ابتداي فايل قرار گيرد و مابقي تنظيمات پس از آن.
يا اگر علاقمند باشيد كه از SQL Server CE استفاده كنيد، تنظيمات رشته اتصالي را به نحو زير مقدار دهي نمائيد:
<connectionStrings> <add name="MyContextName" connectionString="Data Source=|DataDirectory|\Store.sdf" providerName="System.Data.SqlServerCe.4.0" /> </connectionStrings>
در هر دو حالت، name بايد به نام كلاس مشتق شده از DbContext اشاره كند كه در مثال جاري همان Context است.
يا اگر علاقمند بوديد كه اين قرارداد توكار را تغيير داده و نام رشته اتصالي را با كدنويسي تعيين كنيد، ميتوان به نحو زير عمل كرد:
public class Context : DbContext { public Context() : base("ConnectionStringName") { }
البته ضرورتي ندارد اين بانك اطلاعاتي از پيش موجود باشد. در اولين بار اجراي كدهاي زير، به صورت خودكار بانك اطلاعاتي و جداول Blogs و Posts و روابط بين آنها تشكيل ميگردد:
using EF_Sample01.Models; namespace EF_Sample01 { class Program { static void Main(string[] args) { using (var db = new Context()) { db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" }); db.SaveChanges(); } } } }
در اين تصوير چند نكته حائز اهميت هستند:
الف) نام پيش فرض بانك اطلاعاتي كه به آن اشاره شد (اگر تنظيمات رشته اتصالي قيد نگردد).
ب) تشكيل خودكار primary key از روي خواصي به نام Id
ج) تشكيل خودكار روابط بين جداول و ايجاد كليد خارجي (به كمك خاصيت virtual تعريف شده)
د) تشكيل جدول سيستمي به نام dbo.__MigrationHistory كه از آن براي نگهداري سابقه به روز رسانيهاي ساختار جداول كمك گرفته خواهد شد.
ه) نوع و طول فيلدهاي متني، nvarchar از نوع max است.
تمام اينها بر اساس پيش فرضها و قراردادهاي توكار EF Code first انجام شده است.
در كدهاي تعريف شده نيز، ابتدا يك وهله از شيء Context ايجاد شده و سپس به كمك آن ميتوان به جدول Blogs اطلاعاتي را افزود و در آخر ذخيره نمود. استفاده از using هم دراينجا نبايد فراموش شود، زيرا اگر استثنايي در اين بين رخ دهد، كار پاكسازي منابع و بستن اتصال گشوده شده به بانك اطلاعاتي به صورت خودكار انجام خواهد شد.
در ادامه اگر بخواهيم مطلبي را به Blog ثبت شده اضافه كنيم، خواهيم داشت:
using EF_Sample01.Models; namespace EF_Sample01 { class Program { static void Main(string[] args) { //addBlog(); addPost(); } private static void addPost() { using (var db = new Context()) { var blog = db.Blogs.Find(1); db.Posts.Add(new Post { Blog = blog, Content = "data", Title = "EF" }); db.SaveChanges(); } } private static void addBlog() { using (var db = new Context()) { db.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" }); db.SaveChanges(); } } } }
متد db.Blogs.Find، بر اساس primary key بلاگ ثبت شده، يك وهله از آنرا يافته و سپس از آن جهت تشكيل شيء Post و افزودن آن به جدول Posts استفاده ميشود. متد Find ابتدا Contxet جاري را جهت يافتن شيءايي با id مساوي يك جستجو ميكند (اصطلاحا به آن first level cache هم گفته ميشود). اگر موفق به يافتن آن شد، بدون صدور كوئري اضافهاي به بانك اطلاعاتي از اطلاعات همان شيء استفاده خواهد كرد. در غيراينصورت نياز خواهد داشت تا ابتدا كوئري لازم را به بانك اطلاعاتي ارسال كرده و اطلاعات شيء Blog متناظر با id=1 را دريافت كند. همچنين اگر نياز داشتيم تا تنها با سطح اول كش كار كنيم، در EF Code first ميتوان از خاصيتي به نام Local نيز استفاده كرد. براي مثال خاصيت db.Blogs.Local بيانگر اطلاعات موجود در سطح اول كش ميباشد.
نهايتا كوئري Insert توليد شده توسط آن به شكل زير خواهد بود (لاگ شده توسط برنامه SQL Server Profiler):
exec sp_executesql N'insert [dbo].[Posts]([Title], [Content], [Blog_Id]) values (@0, @1, @2) select [Id] from [dbo].[Posts] where @@ROWCOUNT > 0 and [Id] = scope_identity()', N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int', @0=N'EF', @1=N'data', @2=1
اين نوع كوئرهاي پارامتري چندين مزيت مهم را به همراه دارند:
الف) به صورت خودكار تشكيل ميشوند. تمام كوئريهاي پشت صحنه EF پارامتري هستند و نيازي نيست مرتبا مزاياي اين امر را گوشزد كرد و باز هم عدهاي با جمع زدن رشتهها نسبت به نوشتن كوئريهاي نا امن SQL اقدام كنند.
ب) كوئرهاي پارامتري در مقابل حملات تزريق اس كيوال مقاوم هستند.
ج) SQL Server با كوئريهاي پارامتري همانند رويههاي ذخيره شده رفتار ميكند. يعني query execution plan محاسبه شده آنها را كش خواهد كرد. همين امر سبب بالا رفتن كارآيي برنامه در فراخوانيهاي بعدي ميگردد. الگوي كلي مشخص است. فقط پارامترهاي آن تغيير ميكنند.
د) مصرف حافظه SQL Server كاهش مييابد. چون SQL Server مجبور نيست به ازاي هر كوئري اصطلاحا Ad Hoc رسيده يكبار execution plan متفاوت آنها را محاسبه و سپس كش كند. اين مورد مشكل مهم تمام برنامههايي است كه از كوئريهاي پارامتري استفاده نميكنند؛ تا حدي كه گاهي تصور ميكنند شايد SQL Server دچار نشتي حافظه شده، اما مشكل جاي ديگري است.
مشكل! ساختار بانك اطلاعاتي تشكيل شده مطلوب كار ما نيست.
تا همينجا با حداقل كدنويسي و تنظيمات مرتبط با آن، پيشرفت خوبي داشتهايم؛ اما نتيجه حاصل آنچنان مطلوب نيست و نياز به سفارشي سازي دارد. براي مثال طول فيلدها را نياز داريم به مقدار ديگري تنظيم كنيم، تعدادي از فيلدها بايد به صورت not null تعريف شوند يا نام پيش فرض بانك اطلاعاتي بايد مشخص گردد و مواردي از اين دست. با اين موارد در قسمتهاي بعدي بيشتر آشنا خواهيم شد.