Globalization در ASP.NET MVC - قسمت ششم
در مورد روشهای کاهش حجم لاگ فایلهای SQL Server در این مطلب بحث شد.
اما یکی از دیتابیسهای قدیمی shrink نمیشد و پیغام خطای زیر را صادر میکرد:
Cannot shrink log file 2 because of minimum log space required.
یکی از علتهایی که اگر مطابق روش ذکر شده در مقاله یاده شده رفتار شود، سبب کاهش حجم لاگ فایل یک دیتابیس نمیشود، وجود تراکنشهای کامل نشده است. جهت مشاهدهی وضعیت تراکنشهای یک دیتابیس میتوان دستور زیر را صادر کرد:
DBCC OPENTRAN
Replicated Transaction Information:
Oldest distributed LSN : (0:0:0)
Oldest non-distributed LSN : (5291:25:1)
وجود سطر مربوط به Oldest non-distributed LSN به این معنا است که هنوز یک replication نا تمام بر روی این دیتابیس موجود است. البته چون این دیتابیس از یک سرور دیگر به اینجا منتقل شده بود و هیچ نوع replication ایی هم در این سرور بر روی آن تنظیم نشده بود؛ بنابراین ابتدا این replication حذف شد:
exec sp_removedbreplication 'dbName', 'both';
سپس مجددا دستور زیر جهت مشاهدهی وضعیت تراکنشهای ناتمام صادر شد:
DBCC OPENTRAN
که اینبار دیگر هیچ خروجی نداشت.
اکنون با استفاده از روش ذکر شده، لاگ فایل 70 گیگابایتی این دیتابیس به سادگی به چند مگابایت shrink شد.
بررسی حالتهای مختلف نصب RavenDB
الف) استفاده از RavenDB در حالت مدفون شده یا Embedded
حالت Embedded به این معنا است که RavenDB درون پروسه برنامه شما اجرا خواهد شد و نه به صورت پروسهای مجزا. این حالت برای ارائه ساده برنامههای دسکتاپ بسیار مناسب است؛ یا حتی توزیع برنامههای سبک ASP.NET بدون نیاز به نصب بانک اطلاعاتی خاصی بر روی وب سرور.
برای کار با RavenDB در حالت Embedded ابتدا فایلهای مورد نیاز آنرا از طریق نیوگت دریافت کنید:
PM> Install-Package RavenDB.Embedded -Pre
var documentStore = new EmbeddableDocumentStore { DataDirectory = @"~/app_data/ravendb" }; documentStore.Initialize();
امکان تعریف DataDirectory در فایل کانفیگ برنامه نیز وجود دارد. فقط در این حالت باید دقت داشت که نام مسیر، با DataDir شروع میشود و نه DataDirectory :
<connectionStrings> <add name="Local" connectionString="DataDir = ~\Data"/>
چند نکته جالب در مورد حالت Embedded
- امکان اجرای درون حافظهای RavenDB نیز وجود دارد:
var documentStore = new EmbeddableDocumentStore{RunInMemory = true}.Initialize()
- اجرای حالت Embedded به صورت Embedded HTTP:
در حالت Embedded دیگر دسترسی به برنامه سیلورلایت Raven studio وجود ندارد. اگر علاقمند به کار با آن بودید، خاصیت UseEmbeddedHttpServer شیء EmbeddableDocumentStore را به true تنظیم کنید. سپس فایل Raven.Studio.xap را در ریشه وب سایت خود قرار دهید. اکنون مانند قبل آدرس localhost:8080/raven/studio.html برقرار خواهد بود.
همچنین سرور Http این بانک اطلاعاتی را نیز میتوان دستی راه اندازی کرد. متد NonAdminHttp.EnsureCanListenToWhenInNonAdminContext بررسی میکند که آیا برنامه مجوز راه اندازی یک سرور را بر روی پورت مثلا 8080 دارد یا خیر.
NonAdminHttp.EnsureCanListenToWhenInNonAdminContext(8080); // Start the HTTP server manually var server = new RavenDbHttpServer(documentStore.Configuration, documentStore.DocumentDatabase); server.Start();
ب) نصب RavenDB به صورت سرویس ویندوز NT
اگر مایل باشیم تا RavenDB را نیز مانند SQL Server به صورت یک سرویس ویندوز NT نصب کنیم تا همواره در پس زمینه سرور در حال اجرا باشد، کنسول پاورشل ویندوز را گشوده و سپس فرمان ذیل را صادر کنید:
d:\ravendb\server> .\raven.server.exe /install
و اگر خواستید این سرویس را عزل کنید، دستور ذیل را در پاورشل ویندوز صادر کنید:
d:\ravendb\server> .\raven.server.exe /uninstall
ج) نصب RavenDB به صورت یک پروسه IIS (یا اجرا شده توسط IIS)
فایلهای مورد نیاز حالت اجرای RavenDB را به صورت یک پروسه مجزای IIS از نیوگت دریافت کنید:
PM> Install-Package RavenDB.AspNetHost -Pre
یک نکته
تمام بستههای مورد نیاز را یکجا از آدرس http://ravendb.net/download نیز میتوان دریافت کرد. در نگارشهای جدید، بسته نصاب نیز برای این بانک اطلاعاتی تهیه شده است که برای نمونه توزیع آنرا جهت حالت نصب در IIS سادهتر میکند.
مدتی قبل مطلبی تحت عنوان "What’s coming in the next version of ASP.NET Webforms" منتشر شد (که نویسنده آن دقیقا مشخص نیست این اطلاعات را از کجا آورده و همچنین تکذیبیهای هم جایی در مورد آن صادر نشد ...)؛ بنابراین خلاصهای از آنرا با هم مرور خواهیم کرد:
اخیرا تمام توجه تیم ASP.NET معطوف نسخهی MVC آن شده است؛ هر چند هنوز تعداد قابل توجهی از پروژههای ASP.NET بر اساس Webforms تهیه شدهاند یا میشوند. همچنین برخلاف مطالب منتشره در انجمنها یا بلاگهای مرتبط، تیم ASP.NET ، نگارش Webforms را فراموش نکرده و حتی نگارش 4 آن نیز تعدادی از قابلیتهای MVC مانند URL Routing، حجم کمتر ViewState و کنترل بیشتر بر روی HTML نهایی را به همراه داشته است.
به روز رسانیهای متوالی MVC (که اکنون به نگارش 3 رسیده است)، شاید این تصور را پیش آورده باشد که دیگر Webforms مرده است! اما مهترین دلیل به روز رسانیهای دیر هنگام نسخهی Webforms ، یکی بودن اسمبلیهای آن با مجموعهی اصلی دات نت فریم ورک است (برخلاف نسخهی MVC که به صورت افزونهای برای این مجموعه ارائه شده است).
نسخهی بعدی Webforms (حداقل) شامل تازهها و پیشرفتهای زیر خواهد بود:
MVC ModelBinders
در نسخهی MVC مفهومی به نام Model binders وجود دارد. کار آن مقدار دهی مدل برنامه به صورت خودکار بر اساس اطلاعات وارد شده توسط کاربر در رابط کاربری برنامه است. برای مثال در Webforms داریم employee.Name = txtName.Text . به این معنا که مقدر Text یک جعبهی متنی به نام txtName را به خاصیت Name شیء employee نسبت بده. اینکار (انقیاد اطلاعات رابط کاربر به مدل برنامه) با وجود Model binders در نسخهی MVC به صورت خودکار انجام میشود. این مورد دو مزیت عمده را به همراه خواهد داشت: الف) سادگی و حجم کمتر کد ب) امکان تهیه سادهتر unit test جهت قسمتهای مختلف برنامه (چون دیگر به txtName گره نخواهد خورد).
امکانات Model binders ، گفته شده (مطابق مرجع فوق!) که قرار است جزئی از نگارش بعدی Webforms باشد ... (امیدوارم!)
بهبودهای حاصل شده در اعتبار سنجی
نسخهی بعدی Webforms شامل پیشرفتهای اعتبارسنجی نسخهی MVC نیز خواهد بود. به این معنا که امکان کنارگذاشتن کنترلهای اعتبار سنجی Webforms و استفاده یکپارچه از امکانات jQuery فراهم خواهد شد (به این صورت دیگر شما محدود به یک سری کنترل از پیش تعیین شده نخواهید بود و امکان دسترسی به کوهی از افزونههای اعتبار سنجی jQuery را خواهید داشت).
CSS Sprites
CSS Sprites که در نگارش بعدی Webforms پشتیبانی خواهد شد (+)، تکنیکی است جهت کاهش تعداد رفت و برگشتهای به سرور با ارائهی یک فایل حاوی تمام تصاویر قرار گرفته شده در یک شبکه یا گرید. به این صورت بجای دها یا صدها رفت و برگشت به سرور جهت دریافت تصاویر یک صفحه، تنها یک رفت و برگشت انجام خواهد شد.
فرض کنید یک صفحهی Blazor SSR، از سه کامپوننت منوی سمت راست، محتوای اصلی صفحه و فوتر سایت که به همراه متنی است، تشکیل شدهاست. منوی سمت راست، به همراه لینکهاییاست که آمار آنها را نیز نمایش میدهد و این اطلاعات را از بانک اطلاعاتی، به کمک EF-Core دریافت میکند. فوتر صفحه، سال شروع به کار و نام برنامه را از بانک اطلاعاتی دریافت میکند و محتوای اصلی صفحه نیز از بانک اطلاعاتی دریافت میشود. پس از تکمیل این سه کامپوننت مجزا، اگر برنامه را اجرا کنید، بلافاصله با خطای زیر مواجه میشوید:
A second operation started on this context before a previous operation completed
مشکل کجاست؟! مشکل اینجاست که تنها یک نمونه از DbContext، در طول درخواست جاری رسیده، بین سه کامپوننت جاری برنامه به اشتراک گذاشته میشود (به سازندهی سرویسهای مرتبط تزریق میشود) و ... در Blazor SSR، پردازش کامپوننتهای یک صفحه، به صورت موازی و همزمان انجام میشوند؛ یعنی ترتیبی نیست. اگر ابتدا کامپوننت منو، بعد محتوای صفحه و در آخر فوتر، رندر میشدند، هیچگاه پیام فوق را مشاهده نمیکردیم؛ اما ... هر سه کامپوننت، با هم و همزمان رندر میشوند و سپس نتیجهی نهایی در Response درج خواهد شد. یعنی یک DbContext بین چندین ترد به اشتراک گذاشته میشود که چنین حالتی توسط EF-Core پشتیبانی نمیشود و مجاز نیست.
روش مواجه شدن با یک چنین حالتهایی، نمونه سازی مجزای DbContext به ازای هر کامپوننت است که نمونهای از آنرا پیشتر در مطلب «نکات ویژهی کار با EF-Core در برنامههای Blazor Server» مشاهده کردهاید. در این مطلب، راهحل دیگری برای اینکار ارائه میشود که سادهتر است و نیازی به تغییرات آنچنانی در کدهای کامپوننتها و کل برنامه ندارد.
استفاده از کلاس پایهی OwningComponentBase برای نمونه سازی مجدد DbContext بهازای هر کامپوننت
زمانیکه در برنامههای Blazor SSR از روش استاندارد زیر برای دسترسی به سرویسهای مختلف برنامه استفاده میکنیم:
@inject IHotelRoomService HotelRoomService
طول عمر دریافتی سرویس، دقیقا بر اساس طول عمر اصلی تعریف شدهی آن عمل میکند (شبیه به برنامههای ASP.NET Core). یعنی برای مثال اگر Scoped باشد، DbContext تزریق شدهی در آن هم Scoped است و این DbContext، بین تمام کامپوننتهای در حال پردازش موازی در طول یک درخواست، بهاشتراک گذاشته میشود که مطلوب ما نیست. ما میخواهیم بتوانیم به ازای هر کامپوننت مجزای صفحه، یک DbContext جدید داشته باشیم. یعنی باید بتوانیم خودمان این سرویس Scoped را نمونه سازی کنیم و نه اینکه آنرا مستقیما از سیستم تزریق وابستگیها دریافت کنیم.
بنابراین اگر بخواهیم قسمتهای مختلف برنامه را تغییر ندهیم و همان تعاریف ابتدایی services.AddDbContext و Scoped تعریف کردن سرویسهای برنامه بدون تغییر باقی بمانند (و از IDbContextFactory و موارد مشابه دیگر مطلب «نکات ویژهی کار با EF-Core در برنامههای Blazor Server» هم استفاده نکنیم)، باید جایگزینی را برای نمونه سازی سرویسها ارائه دهیم. به همین جهت در ابتدا، یک ویژگی جدیدی را به صورت زیر تعریف میکنیم:
[AttributeUsage(AttributeTargets.Property)] public sealed class InjectComponentScopedAttribute : Attribute { }
تا بتوانیم بجای:
@inject IHotelRoomService HotelRoomService
بنویسیم:
[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;
مرحلهی بعد، نوبت به نمونه سازی خودکار این سرویسهای درخواستی علامتگذاری شدهی با InjectComponentScoped است. برای این منظور، تمام کامپوننتهای برنامه را از کلاس پایه و استاندارد OwningComponentBase ارثبری میکنیم. مزیت اینکار، امکان دسترسی به خاصیتی به نام ScopedServices در تمام کامپوننتهای برنامه است که توسط آن میتوان به متد ScopedServices.GetRequiredService آن دسترسی یافت. یعنی با ارثبری از کلاس پایهی OwningComponentBase به ازای هر کامپوننت، به صورت خودکار Scope جدیدی شروع میشود که توسط آن میتوان به نمونهی جدیدی از سرویس مدنظر دسترسی یافت و نه به نمونهی اشتراکی در طی درخواست جاری.
اکنون اگر از این مزیت به صورت زیر استفاده کنیم، میتوان تمام سرویسهای درخواستی مزین به InjectComponentScopedAttribute یک کامپوننت را به صورت خودکار یافته و با استفاده از ScopedServices.GetRequiredService، مقدار دهی کرد:
public class BlazorScopedComponentBase : OwningComponentBase { private static readonly ConcurrentDictionary<Type, Lazy<List<PropertyInfo>>> CachedProperties = new(); private List<PropertyInfo> InjectComponentScopedPropertiesList => CachedProperties.GetOrAdd(GetType(), type => new Lazy<List<PropertyInfo>>( () => type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) .Where(p => p.GetCustomAttribute<InjectComponentScopedAttribute>() is not null) .ToList(), LazyThreadSafetyMode.ExecutionAndPublication)).Value; protected override void OnInitialized() { foreach (var propertyInfo in InjectComponentScopedPropertiesList) { propertyInfo.SetValue(this, ScopedServices.GetRequiredService(propertyInfo.PropertyType)); } } }
این سرویس، اینبار طول عمری، محدود به کامپوننت جاری را خواهد داشت و بین سایر کامپوننتهای درحال پردازش درخواست جاری، به اشتراک گذاشته نمیشود و همچنین به صورت خودکار هم در پایان درخواست، Dispose میشود.
فعالسازی ارثبری خودکار در تمام کامپوننتهای برنامه
مرحلهی بعد، ارثبری خودکار تمام کامپوننتهای برنامه از OwningComponentBase سفارشی فوق است و در اینجا قصد نداریم تمام کامپوننتها را جهت معرفی آن، به صورت دستی تغییر دهیم. برای اینکار فقط کافی است به فایل Imports.razor_ مراجعه و یک سطر زیر را در آن درج کنیم:
@inherits BlazorScopedComponentBase
با اینکار یک ارثبری سراسری در کل برنامه رخ میدهد و تمام کامپوننتها، از BlazorScopedComponentBase مشتق خواهند شد. یعنی پس از این تغییر، اگر سرویسی را به صورت زیر معرفی و با ویژگی InjectComponentScoped علامتگذاری کردیم:
[InjectComponentScoped] internal IHotelRoomService HotelRoomService { set; get; } = null!;
به صورت خودکار یافت شده و نمونه سازی Scoped محدود به طول عمر همان کامپوننت میشود که بین سایر کامپوننتها، به اشتراک گذاشته نخواهد شد.
یک نکته: اگر کامپوننت شما متد OnInitialized را بازنویسی میکند، فراموش نکنید که در ابتدای آن باید ()base.OnInitialized را هم فراخوانی کنید تا متد OnInitialized کامپوننت پایهی BlazorScopedComponentBase نیز فراخوانی شود. البته این مورد در حین بازنویسی نمونهی async آن مهم نیست؛ چون همیشه OnInitialized غیر async در ابتدا فراخوانی میشود و سپس نمونهی async آن اجرا خواهد شد.
NET Core 2.1.2. منتشر شد
.NET Core 2.1.2 comprises:
- .NET Core Runtime 2.1.2 (release notes)
- .NET Core SDK 2.1.302
| SDK Installer | SDK Binaries | Runtime Installer | Runtime Binaries | ASPNET Core Runtime |
---|---|---|---|---|---|
Windows | 32-bit / 64-bit | 32-bit / 64-bit | 32-bit / 64-bit | 32-bit / 64-bit | |
macOS | 64-bit | 64-bit | 64-bit | 64-bit | |
Linux (for glibc based OS) | See installations steps below | 64-bit | - | 64-bit | 64-bit |
Linux ARM (for musl based OS, such as Alpine) | - | 64-bit | - | 64-bit | 64-bit |
Linux ARM | - | 32-bit / 64-bit | - | 32-bit / 64-bit | 32-bit |
RHEL6 | - | 64-bit | - | 64-bit | |
NET Core 2.1.1. منتشر شد
.NET Core 2.1.1 comprises:
- .NET Core Runtime 2.1.1
- .NET Core SDK 2.1.301
| SDK Installer | SDK Binaries | Runtime Installer | Runtime Binaries | ASPNET Core Runtime |
---|---|---|---|---|---|
Windows | 32-bit / 64-bit | 32-bit / 64-bit | 32-bit / 64-bit | 32-bit / 64-bit | - |
macOS | 64-bit | 64-bit | 64-bit | 64-bit | - |
Linux * | See installations steps below | 64-bit | - | 64-bit | - |
RHEL6 | - | 64-bit | - | 64-bit | - |
Linux ARM (glibc) | - | 32-bit / 64-bit | - | 32-bit / 64-bit | 32-bit |
Linux ARM (musl) | - | 64-bit | 64-bit | 64-bit | 64-bit |
EF Code First #5
در قسمت قبل خاصیت AutomaticMigrationsEnabled را در کلاس Configuration به true تنظیم کردیم. به این ترتیب، عملیات ساده شده، اما یک سری از قابلیتهای ردیابی تغییرات را از دست خواهیم داد و این عملیات، صرفا یک عملیات رو به جلو خواهد بود.
اگر AutomaticMigrationsEnabled را مجددا به false تنظیم کنیم و هربار به کمک دستوارت Add-Migration و Update-Database تغییرات مدلها را به بانک اطلاعاتی اعمال نمائیم، علاوه بر تشکیل تاریخچه این تغییرات در برنامه، امکان بازگشت به عقب و لغو تغییرات صورت گرفته نیز مهیا میگردد.
هدف قرار دادن مرحلهای خاص یا لغو آن
به همان پروژه قسمت قبل مراجعه نمائید. در کلاس Configuration آن، خاصیت AutomaticMigrationsEnabled را به false تنظیم کنید. سپس یک خاصیت جدید را به کلاس Project اضافه نموده و برنامه را اجرا نمائید. بلافاصله خطای زیر را دریافت خواهیم کرد:
Unable to update database to match the current model because there are pending changes and
automatic migration is disabled. Either write the pending model changes to a code-based migration
or enable automatic migration. Set DbMigrationsConfiguration.AutomaticMigrationsEnabled to true
to enable automatic migration.
EF تشخیص داده است که کلاس مدل برنامه، با بانک اطلاعاتی تطابق ندارد و همچنین ویژگی مهاجرت خودکار نیز فعال نیست. بنابراین اعمال code-based migration را توصیه کرده است.
برای این منظور به کنسول پاورشل NuGet مراجعه نمائید (منوی Tools در ویژوال استودیو، گزینه Library package manager آن و سپس انتخاب گزینه package manager console). در ادامه فرمان add-m را نوشته و دکمه tab را فشار دهید. یک منوی Auto Complete ظاهر خواهد شد که از آن میتوان فرمان add-migration را انتخاب نمود. در اینجا یک نام را هم نیاز است وارد کرد؛ برای مثال:
Add-Migration AddSomeProp2ToProject
به این ترتیب کلاس زیر را به صورت خودکار تولید خواهد کرد:
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp2ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp", c => c.String());
AddColumn("Projects", "SomeProp2", c => c.String());
}
public override void Down()
{
DropColumn("Projects", "SomeProp2");
DropColumn("Projects", "SomeProp");
}
}
}
مدلهای برنامه را با بانک اطلاعاتی تطابق داده و دریافته است که هنوز دو خاصیت در اینجا به بانک اطلاعاتی اضافه نشدهاند.
از متد Up برای اعمال تغییرات و از متد Down برای بازگشت به قبل استفاده میگردد. نام فایل این کلاس هم طبق معمول چیزی است شبیه به timeStamp_AddSomeProp2ToProject.cs .
در ادامه نیاز است این تغییرات به بانک اطلاعاتی اعمال شوند. به همین منظور دستور زیر را در کنسول پاورشل وارد نمائید:
Update-Database -Verbose
پارامتر Verbose آن سبب خواهد شد تا جزئیات عملیات به صورت مفصل گزارش داده شود که شامل دستورات ALTER TABLE نیز هست:
Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Applying explicit migrations: [201205061835024_AddSomeProp2ToProject].
Applying explicit migration: 201205061835024_AddSomeProp2ToProject.
ALTER TABLE [Projects] ADD [SomeProp] [nvarchar](max)
ALTER TABLE [Projects] ADD [SomeProp2] [nvarchar](max)
[Inserting migration history record]
اکنون مجددا یک خاصیت دیگر را مثلا به نام public string SomeProp3، به کلاس Project اضافه نمائید.
سپس همین روال باید مجددا تکرار شود. دستورات زیر را در کنسول پاورشل NuGet اجرا نمائید:
Add-Migration AddSomeProp3ToProject
Update-Database -Verbose
اینبار نیز یک کلاس جدید به نام AddSomeProp3ToProject به پروژه اضافه خواهد شد و سپس بر اساس آن، امکان به روز رسانی بانک اطلاعاتی میسر میگردد.
در ادامه برای مثال به این نتیجه رسیدهایم که نیازی به خاصیت public string SomeProp3 اضافه شده، نبوده است. روش متداول، باز هم مانند سابق است. ابتدا خاصیت را از کلاس Project حذف خواهیم کرد و سپس دو دستور Add-Migration و Update-Database را اجرا خواهیم نمود.
اما با توجه به اینکه مهاجرت خودکار را غیرفعال کردهایم و هربار با فراخوانی دستور Add-Migration یک کلاس جدید، با متدهای Up و Down به پروژه، جهت نگهداری سوابق عملیات اضافه میشوند، میتوان دستور Update-Database را جهت فراخوانی متد Down صرفا یک مرحله موجود نیز فراخوانی نمود.
نکته:
اگر علاقمند باشید که راهنمای مفصل پارامترهای دستور Update-Database را مشاهده کنید، تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائید:
get-help update-database -detailed
به عنوان نمونه اگر در حین فراخوانی دستور Update-Database احتمال از دست رفتن اطلاعات باشد، عملیات متوقف میشود. برای وادار کردن پروسه به انجام تغییرات بر روی بانک اطلاعاتی میتوان از پارامتر Force در اینجا استفاده کرد.
در ادامه برای اینکه دستور Update-Database تنها یک مرحله مشخص را که سابقه آن در برنامه موجود است، هدف قرار دهد، باید از پارامتر TargetMigration به همراه نام کلاس مرتبط استفاده کرد:
Update-Database -TargetMigration:"AddSomeProp2ToProject" -Verbose
اگر دقت کرده باشید در اینجا AddSomeProp2ToProject بجای AddSomeProp3ToProject بکارگرفته شده است. اگر یک مرحله قبل را هدف قرار دهیم، متد Down را اجرا خواهد کرد:
Using NuGet project 'EF_Sample02'.
Using StartUp project 'EF_Sample02'.
Target database is: 'testdb2012' (DataSource: (local), Provider: System.Data.SqlClient, Origin: Configuration).
Reverting migrations: [201205061845485_AddSomeProp3ToProject].
Reverting explicit migration: 201205061845485_AddSomeProp3ToProject.
DECLARE @var0 nvarchar(128)
SELECT @var0 = name
FROM sys.default_constraints
WHERE parent_object_id = object_id(N'Projects')
AND col_name(parent_object_id, parent_column_id) = 'SomeProp3';
IF @var0 IS NOT NULL
EXECUTE('ALTER TABLE [Projects] DROP CONSTRAINT ' + @var0)
ALTER TABLE [Projects] DROP COLUMN [SomeProp3]
[Deleting migration history record]
همانطور که ملاحظه میکنید در اینجا عملیات حذف ستون SomeProp3 انجام شده است. البته این خاصیت به صورت خودکار از کدهای برنامه (کلاس Project در این مثال) حذف نمیشود و فرض بر این است که پیشتر اینکار را انجام دادهاید.
سفارشی سازی کلاسهای مهاجرت
تمام کلاسهای خودکار مهاجرت تولید شده توسط پاورشل، از کلاس DbMigration ارث بری میکنند. در این کلاس امکانات قابل توجهی مانند AddColumn، AddForeignKey، AddPrimaryKey، AlterColumn، CreateIndex و امثال آن وجود دارند که در تمام کلاسهای مشتق شده از آن، قابل استفاده هستند. حتی متد Sql نیز در آن پیش بینی شده است که در صورت نیاز به اجرای دستوارت خام SQL، میتوان از آن استفاده کرد.
برای مثال فرض کنید مجددا همان خاصیت public string SomeProp3 را به کلاس Project اضافه کردهایم. اما اینبار نیاز است حین تشکیل این فیلد در بانک اطلاعاتی، یک مقدار پیش فرض نیز برای آن درنظر گرفته شود که در صورت نال بودن مقدار خاصیت آن در برنامه، به صورت خودکار توسط بانک اطلاعاتی مقدار دهی گردد:
namespace EF_Sample02.Migrations
{
using System.Data.Entity.Migrations;
public partial class AddSomeProp3ToProject : DbMigration
{
public override void Up()
{
AddColumn("Projects", "SomeProp3", c => c.String(defaultValue: "some data"));
Sql("Update Projects set SomeProp3=N'some data'");
}
public override void Down()
{
DropColumn("Projects", "SomeProp3");
}
}
}
متد String در اینجا چنین امضایی دارد:
public ColumnModel String(bool? nullable = null, int? maxLength = null, bool? fixedLength = null,
bool? isMaxLength = null, bool? unicode = null, string defaultValue = null, string defaultValueSql = null,
string name = null, string storeType = null)
که برای نمونه در اینجا پارامتر defaultValue آنرا در کلاس AddSomeProp3ToProject مقدار دهی کردهایم.
برای اعمال این تغییرات تنها کافی است دستور Update-Database -Verbose اجرا گردد. اینبار خروجی SQL اجرا شده آن به نحو زیر است که شامل مقدار پیش فرض نیز شده است:
ALTER TABLE [Projects] ADD [SomeProp3] [nvarchar](max) DEFAULT 'some data'
تعیین مقدار پیش فرض، زمانیکه یک فیلد not null تعریف شدهاست نیز میتواند مفید باشد. همچنین در اینجا امکان اجرای دستورات مستقیم SQL نیز وجود دارد که نمونهای از آنرا در متد Up فوق مشاهده میکنید.
افزودن رکوردهای پیش فرض در حین به روز رسانی بانک اطلاعاتی
در قسمتهای قبل با متد Seed که به همراه آغاز کنندههای بانک اطلاعاتی EF ارائه شدهاند، جهت افزودن رکوردهای اولیه و پیش فرض به بانک اطلاعاتی آشنا شدید. در اینجا نیز با تحریف متد Seed در کلاس Configuration، چنین امری میسر است:
namespace EF_Sample02.Migrations
{
using System;
using System.Data.Entity.Migrations;
internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
public Configuration()
{
this.AutomaticMigrationsEnabled = false;
this.AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(EF_Sample02.Sample2Context context)
{
context.Users.AddOrUpdate(
a => a.Name,
new Models.User { Name = "Vahid", AddDate = DateTime.Now },
new Models.User { Name = "Test", AddDate = DateTime.Now });
}
}
}
متد AddOrUpdate در EF 4.3 اضافه شده است. این متد ابتدا بررسی میکند که آیا رکورد مورد نظر در بانک اطلاعاتی وجود دارد یا خیر. اگر خیر، آنرا اضافه خواهد کرد در غیراینصورت، نمونه موجود را به روز رسانی میکند. اولین پارامتر آن، identifierExpression نام دارد. توسط آن مشخص میشود که بر اساس چه خاصیتی باید در مورد update یا add تصمیمگیری شود. دراینجا اگر نیاز به ذکر بیش از یک خاصیت وجود داشت، از anonymously type object میتوان کمک گرفت new { p.Name, p.LastName } .
تولید اسکریپت به روز رسانی بانک اطلاعاتی
بهترین کار و امنترین روش حین انجام این نوع به روز رسانیها، تهیه اسکریپت SQL فرامینی است که باید بر روی بانک اطلاعاتی اجرا شوند. سپس میتوان این دستورات و اسکریپت نهایی را دستی هم اجرا کرد (که روش متداولتری است در محیط کاری).
برای اینکار تنها کافی است دستور زیر را در کنسول پاورشل اجرا نمائیم:
Update-Database -Verbose -Script
پس از اجرای این دستور، یک فایل اسکریپت با پسوند sql تولید شده و بلافاصله در ویژوال استودیو جهت مرور نیز گشوده خواهد شد. برای نمونه محتوای آن برای افزودن خاصیت جدید SomeProp5 به صورت زیر است:
ALTER TABLE [Projects] ADD [SomeProp5] [nvarchar](max)
INSERT INTO [__MigrationHistory] ([MigrationId], [CreatedOn], [Model], [ProductVersion]) VALUES
('201205060852004_AutomaticMigration', '2012-05-06T08:52:00.937Z', 0x1F8B0800000............ '4.3.1')
همانطور که ملاحظه میکنید، در یک مرحله، جدول پروژهها را به روز خواهد کرد و در مرحله بعد، سابقه آنرا در جدول __MigrationHistory ثبت میکند.
یک نکته:
اگر دستور فوق را بر روی برنامهای که با بانک اطلاعاتی هماهنگ است اجرا کنیم، خروجی را مشاهده نخواهیم کرد. برای این منظور میتوان مرحله خاصی را توسط پارامتر SourceMigration هدف گیری کرد:
Update-Database -Verbose -Script -SourceMigration:"stepName"
استفاده از DB Migrations در عمل
البته این یک روش پیشنهادی و امن است:
الف) در ابتدای اجرا برنامه، پارامتر ورودی متد System.Data.Entity.Database.SetInitializer را به نال تنظیم کنید تا برنامه تغییری را بر روی بانک اطلاعاتی اعمال نکند.
ب) توسط دستور enable-migrations، فایلهای اولیه DB Migration را ایجاد کنید. پیش فرضهای آن را نیز تغییر ندهید.
ج) هر بار که کلاسهای مدل برنامه تغییر کردند و پس از آن نیاز به به روز رسانی ساختار بانک اطلاعاتی وجود داشت دو دستور زیر را اجرا کنید:
Add-Migration AddSomePropToProject
Update-Database -Verbose -Script
به این ترتیب سابقه تغییرات در برنامه نگهداری شده و همچنین بدون اجرای دستورات بر روی بانک اطلاعاتی، اسکریپت نهایی اعمال تغییرات تولید میگردد.
د) اسکریپت تولید شده را بررسی کرده و پس از تائید و افزودن به سورس کنترل، به صورت دستی بر روی بانک اطلاعاتی اجرا کنید (مثلا توسط management studio).
اما سمت کلاینت شما هر کاری را میتوانید انجام دهید. برای مثال زمان نمایش اطلاعات در WPF یا سیلورلایت از یک Converter استاندارد آن (با پیاده سازی اینترفیس IValueConverter) در حین Binding استفاده کنید. اگر با ASP.NET Webforms کار میکنید حین نمایش اطلاعاتی که هم اکنون در سمت کلاینت مهیا است ، مثلا جهت نمایش در یک GridView یا موارد مشابه شما خواهید داشت myFunc(Eval("field")) و شبیه به این که myFunc باید در کدبیهایند شما پیاده سازی شود. در سایر فناوریها که میتواند شامل موارد قبل هم باشند، نهایتا شما یک لیست دریافتی از سرور را دارید، یک حلقه با LINQ یا حالت معمولی تشکیل شده و مقادیر مدل مورد نظر ویرایش میشوند تا جهت نمایش مناسب شوند.
تمام اینها در حالتی است که قصد شما فقط و فقط تغییر نحوهی نمایش است. به عبارتی الان کل دیتای فیلتر شده سمت کاربر مهیا است. شما میخواهید به آن شکل دهید.
حالت دیگر (حالت غیر نمایشی و استفاده در کوئریها):
اگر با LINQ کمی بیشتر از اطلاعات موجود در وب کار کرده باشید احتمالا به این سوال رسیدهاید که آیا میشود متد سفارشی خودمان را هم حین تهیه کوئریهایی از این دست استفاده کنیم؟ چون فقط یک سری extension method مشخص بیشتر وجود ندارند. اگر من extension method سفارشی خودم را تهیه کردم چطور؟
این سوال دو پاسخ دارد:
- متدهای سفارشی شما حتما روی کل اطلاعات دریافتی از سرور کار میکنند؛ اما بهینه نیستند. چون برای مثال myFunc سی شارپ من معادل SQL ایی ندارد که بتوانم مستقیما آنرا سمت سرور اجرا کنم. چون نهایتا LINQ to NHibernate باید به SQL یا T-SQL ترجمه شود. به همین جهت مجبورم کل اطلاعات را دریافت کنم، مثلا 100 هزار رکورد، حالا که اشیاء دات نتی من تشکیل و کامل شده، متد سفارشی LINQ خودم را بر روی اینها اجرا میکنم. این روش کار میکنه ولی از لحاظ کارآیی فاجعه است.
- روش دیگر: در NH 3.0 این امکان وجود دارد ... بسط پروایدر LINQ آن با صور مختلف. که اگر وقت شد یک مطلب کامل در مورد آن خواهم نوشت.