۱۳۹۱/۰۲/۱۸

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).