۱۳۹۱/۰۲/۱۷

EF Code First #4


آشنايي با Code first migrations

ويژگي Code first migrations براي اولين بار در EF 4.3 ارائه شد و هدف آن سهولت هماهنگ سازي كلاس‌هاي مدل برنامه با بانك اطلاعاتي است؛ به صورت خودكار يا با تنظيمات دقيق دستي.

همانطور كه در قسمت‌هاي قبل نيز به آن اشاره شد، تا پيش از EF 4.3، پنج روال جهت آغاز به كار با بانك اطلاعاتي در EF code first وجود داشت و دارد:
1) در اولين بار اجراي برنامه، در صورتيكه بانك اطلاعاتي اشاره شده در رشته اتصالي وجود خارجي نداشته باشد، نسبت به ايجاد خودكار آن اقدام مي‌گردد. اينكار پس از وهله سازي اولين DbContext و همچنين صدور يك كوئري به بانك اطلاعاتي انجام خواهد شد.
2) DropCreateDatabaseAlways : همواره پس از شروع برنامه، ابتدا بانك اطلاعاتي را drop كرده و سپس نمونه جديدي را ايجاد مي‌كند.
3) DropCreateDatabaseIfModelChanges : اگر EF Code first تشخيص دهد كه تعاريف مدل‌هاي شما با بانك اطلاعاتي مشخص شده توسط رشته اتصالي، هماهنگ نيست، آن‌را drop كرده و نمونه جديدي را توليد مي‌كند.
4) با مقدار دهي پارامتر متد System.Data.Entity.Database.SetInitializer به نال، مي‌توان فرآيند آغاز خودكار بانك اطلاعاتي را غيرفعال كرد. در اين حالت شخص مي‌تواند تغييرات انجام شده در كلاس‌هاي مدل برنامه را به صورت دستي به بانك اطلاعاتي اعمال كند.
5) مي‌توان با پياده سازي اينترفيس IDatabaseInitializer، يك آغاز كننده بانك اطلاعاتي سفارشي را نيز توليد كرد.

اكثر اين روش‌ها در حين توسعه يك برنامه يا خصوصا جهت سهولت انجام آزمون‌هاي خودكار بسيار مناسب هستند، اما به درد محيط كاري نمي‌خورند؛ زيرا drop يك بانك اطلاعاتي به معناي از دست دادن تمام اطلاعات ثبت شده در آن است. براي رفع اين مشكل مهم، مفهومي به نام «Migrations» در EF 4.3 ارائه شده است تا بتوان بانك اطلاعاتي را بدون تخريب آن، بر اساس اطلاعات تغيير كرده‌ي كلاس‌هاي مدل برنامه، تغيير داد. البته بديهي است زمانيكه توسط NuGet نسبت به دريافت و نصب EF اقدام مي‌شود، همواره آخرين نگارش پايدار كه حاوي اطلاعات و فايل‌هاي مورد نياز جهت كار با «Migrations» است را نيز دريافت خواهيم كرد.


تنظيمات ابتدايي Code first migrations

در اينجا قصد داريم همان مثال قسمت قبل را ادامه دهيم. در آن مثال از يك نمونه سفارشي سازي شده DropCreateDatabaseAlways استفاده شد.
نياز است از منوي Tools در ويژوال استوديو، گزينه‌ Library package manager آن، گزينه package manager console را انتخاب كرد تا كنسول پاورشل NuGet ظاهر شود.
اطلاعات مرتبط با پاورشل EF، به صورت خودكار توسط NuGet نصب مي‌شود. براي مثال جهت مشاهده آن‌ها به مسير packages\EntityFramework.4.3.1\tools در كنار پوشه پروژه خود مراجعه نمائيد.
در ادامه در پايين صفحه، زمانيكه كنسول پاورشل NuGet ظاهر مي‌شود، ابتدا بايد دقت داشت كه قرار است فرامين را بر روي چه پروژه‌اي اجرا كنيم. براي مثال اگر تعاريف DbContext را به يك اسمبلي و پروژه class library مجزا انتقال داده‌ايد، گزينه Default project را در اين قسمت بايد به اين پروژه مجزا، تغيير دهيد.
سپس در خط فرمان پاور شل، دستور enable-migrations را وارد كرده و دكمه enter را فشار دهيد.
پس از اجراي اين دستور، يك سري اتفاقات رخ خواهد داد:
الف) پوشه‌اي به نام Migrations به پروژه پيش فرض مشخص شده در كنسول پاورشل، اضافه مي‌شود.
ب) دو كلاس جديد نيز در آن پوشه تعريف خواهند شد به نام‌هاي Configuration.cs و يك نام خودكار مانند number_InitialCreate.cs
ج) در كنسول پاور شل، پيغام زير ظاهر مي‌گردد:
Detected database created with a database initializer. Scaffolded migration '201205050805256_InitialCreate' 
corresponding to current database schema. To use an automatic migration instead, delete the Migrations
folder and re-run Enable-Migrations specifying the -EnableAutomaticMigrations parameter.

با توجه به اينكه در مثال قسمت سوم، از آغاز كننده سفارشي سازي شده DropCreateDatabaseAlways استفاده شده بود، اطلاعات آن در جدول سيستمي dbo.__MigrationHistory در بانك اطلاعاتي برنامه موجود است (تصويري از آن‌را در قسمت اول اين سري مشاهده كرديد). سپس با توجه به ساختار بانك اطلاعاتي جاري، دو كلاس خودكار زير را ايجاد كرده است:

namespace EF_Sample02.Migrations
{
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(EF_Sample02.Sample2Context context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
            //  to avoid creating duplicate seed data. E.g.
            //
            //    context.People.AddOrUpdate(
            //      p => p.FullName,
            //      new Person { FullName = "Andrew Peters" },
            //      new Person { FullName = "Brice Lambson" },
            //      new Person { FullName = "Rowan Miller" }
            //    );
            //
        }
    }
}

namespace EF_Sample02.Migrations
{
    using System.Data.Entity.Migrations;
    
    public partial class InitialCreate : DbMigration
    {
        public override void Up()
        {
            CreateTable(
                "Users",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Name = c.String(),
                        LastName = c.String(),
                        Email = c.String(),
                        Description = c.String(),
                        Photo = c.Binary(),
                        RowVersion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
                        Interests_Interest1 = c.String(maxLength: 450),
                        Interests_Interest2 = c.String(maxLength: 450),
                        AddDate = c.DateTime(nullable: false),
                    })
                .PrimaryKey(t => t.Id);
            
            CreateTable(
                "Projects",
                c => new
                    {
                        Id = c.Int(nullable: false, identity: true),
                        Title = c.String(maxLength: 50),
                        Description = c.String(),
                        RowVesrion = c.Binary(nullable: false, fixedLength: true, timestamp: true, storeType: "rowversion"),
                        AddDate = c.DateTime(nullable: false),
                        AdminUser_Id = c.Int(),
                    })
                .PrimaryKey(t => t.Id)
                .ForeignKey("Users", t => t.AdminUser_Id)
                .Index(t => t.AdminUser_Id);
            
        }
        
        public override void Down()
        {
            DropIndex("Projects", new[] { "AdminUser_Id" });
            DropForeignKey("Projects", "AdminUser_Id", "Users");
            DropTable("Projects");
            DropTable("Users");
        }
    }
}


در اين كلاس خودكار، نحوه ايجاد جداول بانك اطلاعاتي تعريف شده‌اند. در متد تحريف شده Up، كار ايجاد بانك اطلاعاتي و در متد تحريف شده Down، دستورات حذف جداول و قيود ذكر شده‌اند.
به علاوه اينبار متد Seed را در كلاس مشتق شده از DbMigrationsConfiguration، مي‌توان تحريف و مقدار دهي كرد.
علاوه بر اين‌ها جدول سيستمي dbo.__MigrationHistory نيز با اطلاعات جاري مقدار دهي مي‌گردد.


فعال سازي گزينه‌هاي مهاجرت خودكار

براي استفاده از اين كلاس‌ها، ابتدا به فايل Configuration.cs مراجعه كرده و خاصيت AutomaticMigrationsEnabled را true‌ كنيد:

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
        }

پس از آن EF به صورت خودكار كار استفاده و مديريت «Migrations» را عهده‌دار خواهد شد. البته براي اين منظور بايد نوع آغاز كننده بانك اطلاعاتي را از DropCreateDatabaseAlways قبلي به نمونه جديد MigrateDatabaseToLatestVersion نيز تغيير دهيم:
//Database.SetInitializer(new Sample2DbInitializer());
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Sample2Context, Migrations.Configuration>());

يك نكته:
كلاس Migrations.Configuration كه بايد در حين وهله سازي از MigrateDatabaseToLatestVersion قيد شود (همانند كدهاي فوق)، از نوع internal sealed معرفي شده است. بنابراين اگر اين كلاس را در يك اسمبلي جداگانه قرار داده‌ايد، نياز است فايل را ويرايش كرده و internal sealed آن‌را به public تغيير دهيد.

روش ديگر معرفي كلاس‌هاي Context و Migrations.Configuration، حذف متد Database.SetInitializer و استفاده از فايل app.config يا web.config است به نحو زير ( در اينجا حرف ` اصطلاحا back tick نام دارد. فشردن دكمه ~ در حين تايپ انگليسي):

<entityFramework>
    <contexts>
      <context type="EF_Sample02.Sample2Context, EF_Sample02">
        <databaseInitializer
           type="System.Data.Entity.MigrateDatabaseToLatestVersion`2[[EF_Sample02.Sample2Context, EF_Sample02],
               [EF_Sample02.Migrations.Configuration, EF_Sample02]], EntityFramework"
        />
      </context>
    </contexts>
  </entityFramework>

آزمودن ويژگي مهاجرت خودكار

اكنون براي آزمايش اين موارد، يك خاصيت دلخواه را به كلاس Project به نام public string SomeProp اضافه كنيد. سپس برنامه را اجرا نمائيد.
در ادامه به بانك اطلاعاتي مراجعه كرده و فيلدهاي جدول Projects را بررسي كنيد:

CREATE TABLE [dbo].[Projects](
---...
 [SomeProp] [nvarchar](max) NULL,
---...

بله. اينبار فيلد SomeProp بدون از دست رفتن اطلاعات و drop بانك اطلاعاتي، به جدول پروژه‌ها اضافه شده است.


عكس العمل ويژگي مهاجرت خودكار در مقابل از دست رفتن اطلاعات

در ادامه، خاصيت public string SomeProp را كه در قسمت قبل به كلاس پروژه اضافه كرديم، حذف كنيد. اكنون مجددا برنامه را اجرا نمائيد. برنامه بلافاصله با استثناي زير متوقف خواهد شد:

Automatic migration was not applied because it would result in data loss.

از آنجائيكه حذف يك خاصيت مساوي است با حذف يك ستون در جدول بانك اطلاعاتي، امكان از دست رفتن اطلاعات در اين بين بسيار زياد است. بنابراين ويژگي مهاجرت خودكار ديگر اعمال نخواهد شد و اين مورد به نوعي يك محافظت خودكار است كه درنظر گرفته شده است.
البته در EF Code first اين مساله را نيز مي‌توان كنترل نمود. به كلاس Configuration اضافه شده توسط پاورشل مراجعه كرده و خاصيت AutomaticMigrationDataLossAllowed را به true تنظيم كنيد:

internal sealed class Configuration : DbMigrationsConfiguration<EF_Sample02.Sample2Context>
{
        public Configuration()
        {
            this.AutomaticMigrationsEnabled = true;
            this.AutomaticMigrationDataLossAllowed = true;
        }

اين تغيير به اين معنا است كه خودمان صريحا مجوز حذف يك ستون و اطلاعات مرتبط به آن‌را صادر كرده‌ايم.
پس از اين تغيير، مجددا برنامه را اجرا كنيد. ستون SomeProp به صورت خودكار حذف خواهد شد، اما اطلاعات ركوردهاي موجود تغييري نخواهند كرد.


استفاده از Code first migrations بر روي يك بانك اطلاعاتي موجود

تفاوت يك ديتابيس موجود با بانك اطلاعاتي توليد شده توسط EF Code first در نبود جدول سيستمي dbo.__MigrationHistory است.
به اين ترتيب زمانيكه فرمان enable-migrations را در يك پروژه EF code first متصل به بانك اطلاعاتي قديمي موجود اجرا مي‌كنيم، پوشه Migration در آن ايجاد خواهد شد اما تنها حاوي فايل Configuration.cs است و نه فايلي شبيه به number_InitialCreate.cs .
بنابراين نياز است به صورت صريح به EF اعلام كنيم كه نياز است تا جدول سيستمي dbo.__MigrationHistory و فايل number_InitialCreate.cs را نيز توليد كند. براي اين منظور كافي است دستور زير را در خط فرمان پاورشل NuGet پس از فراخواني enable-migrations اوليه، اجرا كنيم:
add-migration Initial -IgnoreChanges

با بكارگيري پارامتر IgnoreChanges، متد Up در فايل number_InitialCreate.cs توليد نخواهد شد. به اين ترتيب نگران نخواهيم بود كه در اولين بار اجراي برنامه، تعاريف ديتابيس موجود ممكن است اندكي تغيير كند.
سپس دستور زير را جهت به روز رساني جدول سيستمي dbo.__MigrationHistory اجرا كنيد:
update-database

پس از آن جهت سوئيچ به مهاجرت خودكار، خاصيت AutomaticMigrationsEnabled = true را در فايل Configuration.cs همانند قبل مقدار دهي كنيد.


مشاهده دستوارت SQL به روز رساني بانك اطلاعاتي

اگر علاقمند هستيد كه دستورات T-SQL به روز رساني بانك اطلاعاتي را نيز مشاهده كنيد، دستور Update-Database را با پارامتر Verbose آغاز نمائيد:
Update-Database -Verbose

و اگر تنها نياز به مشاهده اسكريپت توليدي بدون اجراي آن‌ها بر روي بانك اطلاعاتي مدنظر است، از پارامتر Script بايد استفاده كرد:
update-database -Script



نكته‌اي در مورد جدول سيستمي dbo.__MigrationHistory

تنها دليلي كه اين جدول در SQL Server البته (ونه براي مثال در SQL Server CE) به صورت سيستمي معرفي مي‌شود اين است كه «جلوي چشم نباشد»! به اين ترتيب در SQL Server management studio در بين ساير جداول معمولي بانك اطلاعاتي قرار نمي‌گيرد. اما براي EF تفاوتي نمي‌كند كه اين جدول سيستمي است يا خير.
همين سيستمي بودن آن ممكن است بر اساس سطح دسترسي كاربر اتصالي به بانك اطلاعاتي مساله ساز شود. براي نمونه ممكن است schema كاربر متصل dbo نباشد. همينجا است كه كار به روز رساني اين جدول متوقف خواهد شد.
بنابراين اگر قصد داشتيد خواص سيستمي آن‌را لغو كنيد، تنها كافي است دستورات T-SQL زير را در SQL Server اجرا نمائيد:

SELECT * INTO [TempMigrationHistory]
FROM [__MigrationHistory]
DROP TABLE [__MigrationHistory]
EXEC sp_rename [TempMigrationHistory], [__MigrationHistory]


ساده سازي پروسه مهاجرت خودكار

كل پروسه‌اي را كه در اين قسمت مشاهده كرديد، به صورت ذيل نيز مي‌توان خلاصه كرد:

using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Data.Entity.Migrations.Infrastructure;
using System.IO;

namespace EF_Sample02
{
    public class Configuration<T> : DbMigrationsConfiguration<T> where T : DbContext
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }
    }

    public class SimpleDbMigrations
    {
        public static void UpdateDatabaseSchema<T>(string SQLScriptPath = "script.sql") where T : DbContext
        {
            var configuration = new Configuration<T>();
            var dbMigrator = new DbMigrator(configuration);
            saveToFile(SQLScriptPath, dbMigrator);
            dbMigrator.Update();
        }

        private static void saveToFile(string SQLScriptPath, DbMigrator dbMigrator)
        {
            if (string.IsNullOrWhiteSpace(SQLScriptPath)) return;

            var scriptor = new MigratorScriptingDecorator(dbMigrator);
            var script = scriptor.ScriptUpdate(sourceMigration: null, targetMigration: null);
            File.WriteAllText(SQLScriptPath, script);
            Console.WriteLine(script);
        }
    }
}

سپس براي استفاده از آن خواهيم داشت:

SimpleDbMigrations.UpdateDatabaseSchema<Sample2Context>();

در اين كلاس ذخيره سازي اسكريپت توليدي جهت به روز رساني بانك اطلاعاتي جاري در يك فايل نيز درنظر گرفته شده است.



تا اينجا مهاجرت خودكار را بررسي كرديم. در قسمت بعدي Code-Based Migrations را ادامه خواهيم داد.