۱۳۹۱/۰۲/۲۱

EF Code First #8


ادامه بحث بررسي جزئيات نحوه نگاشت كلاس‌ها به جداول، توسط EF Code first


استفاده از Viewهاي SQL Server در EF Code first

از Viewها عموما همانند يك جدول فقط خواندني استفاده مي‌شود. بنابراين نحوه نگاشت اطلاعات يك كلاس به يك View دقيقا همانند نحوه نگاشت اطلاعات يك كلاس به يك جدول است و تمام نكاتي كه تا كنون بررسي شدند، در اينجا نيز صادق است. اما ...
الف) بر اساس تنظيمات توكار EF Code first، نام مفرد كلاس‌ها، حين نگاشت به جداول، تبديل به اسم جمع مي‌شوند. بنابراين اگر View ما در سمت بانك اطلاعاتي چنين تعريفي دارد:
Create VIEW EmployeesView
AS
SELECT id,
       FirstName
FROM   Employees

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

using System.ComponentModel.DataAnnotations;

namespace EF_Sample04.Models
{
    [Table("EmployeesView")]
    public class EmployeesView
    {
        public int Id { set; get; }
        public string FirstName { set; get; }
    }
}

در اينجا به كمك ويژگي Table، نام دقيق اين View را در بانك اطلاعاتي مشخص كرده‌ايم. به اين ترتيب تنظيمات توكار EF بازنويسي خواهد شد و ديگر به دنبال EmployeesViews نخواهد گشت؛ يا جدول متناظر با آن‌را به صورت خودكار ايجاد نخواهد كرد.
ب) View شما نياز است داراي يك فيلد Primary key نيز باشد.
ج) اگر از مهاجرت خودكار توسط MigrateDatabaseToLatestVersion استفاده كنيم، پيغام خطاي زير را دريافت خواهيم كرد:

There is already an object named 'EmployeesView' in the database.

علت اين است كه هنوز جدول dbo.__MigrationHistory از وجود آن مطلع نشده است، زيرا يك View، خارج از برنامه و در سمت بانك اطلاعاتي اضافه مي‌شود.
براي حل اين مشكل مي‌توان همانطور كه در قسمت‌هاي قبل نيز عنوان شد، EF را طوري تنظيم كرد تا كاري با بانك اطلاعاتي نداشته باشد:

Database.SetInitializer<Sample04Context>(null);

به اين ترتيب EmployeesView در همين لحظه قابل استفاده است.
و يا به حالت امن مهاجرت دستي سوئيچ كنيد:
Add-Migration Init -IgnoreChanges
Update-Database

پارامتر IgnoreChanges سبب مي‌شود تا متدهاي Up و Down كلاس مهاجرت توليد شده، خالي باشد. يعني زمانيكه دستور Update-Database انجام مي‌شود، نه Viewايي دراپ خواهد شد و نه جدول اضافه‌اي ايجاد مي‌گردد. فقط جدول dbo.__MigrationHistory به روز مي‌شود كه هدف اصلي ما نيز همين است.
همچنين در اين حالت كنترل كاملي بر روي كلاس‌هاي Up و Down وجود دارد. مي‌توان CreateTable اضافي را به سادگي از اين كلاس‌ها حذف كرد.

ضمن اينكه بايد دقت داشت يكي از اهداف كار با ORMs، فراهم شدن امكان استفاده از بانك‌هاي اطلاعاتي مختلف، بدون اعمال تغييري در كدهاي برنامه مي‌باشد (فقط تغيير كانكشن استرينگ، به علاوه تعيين Provider جديد، بايد جهت اين مهاجرت كفايت كند). بنابراين اگر از View استفاده مي‌كنيد، اين برنامه به SQL Server گره خواهد خورد و ديگر از ساير بانك‌هاي اطلاعاتي كه از اين مفهوم پشتيباني نمي‌كنند، نمي‌توان به سادگي استفاده كرد.



استفاده از فيلدهاي XML اس كيوال سرور

در حال حاضر پشتيباني توكاري توسط EF Code first از فيلدهاي ويژه XML اس كيوال سرور وجود ندارد؛ اما استفاده از آن‌ها با رعايت چند نكته ساده، به نحو زير است:

using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;

namespace EF_Sample04.Models
{
    public class MyXMLTable
    {
        public int Id { get; set; }

        [Column(TypeName = "xml")]
        public string XmlValue { get; set; }

        [NotMapped]
        public XElement XmlValueWrapper
        {
            get { return XElement.Parse(XmlValue); }
            set { XmlValue = value.ToString(); }
        }
    }
}


در اينجا توسط TypeName ويژگي Column، نوع توكار xml مشخص شده است. اين فيلد در طرف كدهاي كلاس‌هاي برنامه، به صورت string تعريف مي‌شود. سپس اگر نياز بود به اين خاصيت توسط LINQ to XML دسترسي يافت، مي‌توان يك فيلد محاسباتي را همانند خاصيت XmlValueWrapper فوق تعريف كرد. نكته‌ ديگري را كه بايد به آن دقت داشت، استفاده از ويژگي NotMapped مي‌باشد، تا EF سعي نكند خاصيتي از نوع XElement را (يك CLR Property) به بانك اطلاعاتي نگاشت كند.

و همچنين اگر علاقمند هستيد كه اين قابليت به صورت توكار اضافه شود، مي‌توانيد اينجا راي دهيد!



نحوه تعريف Composite keys در EF Code first

كلاس نوع فعاليت زير را درنظر بگيريد:

namespace EF_Sample04.Models
{
    public class ActivityType
    {
        public int UserId { set; get; }
        public int ActivityID { get; set; }
    }
}

در جدول متناظر با اين كلاس، نبايد دو ركورد تكراري حاوي شماره كاربري و شماره فعاليت يكساني باهم وجود داشته باشند. بنابراين بهتر است بر روي اين دو فيلد، يك كليد تركيبي تعريف كرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;

namespace EF_Sample04.Mappings
{
    public class ActivityTypeConfig : EntityTypeConfiguration<ActivityType>
    {
        public ActivityTypeConfig()
        {
            this.HasKey(x => new { x.ActivityID, x.UserId });
        }
    }
}

در اينجا نحوه معرفي بيش از يك كليد را در متد HasKey ملاحظه مي‌كنيد.

يك نكته:
اينبار اگر سعي كنيم مثلا از متد db.ActivityTypes.Find با يك پارامتر استفاده كنيم، پيغام خطاي «The number of primary key values passed must match number of primary key values defined on the entity» را دريافت خواهيم كرد. براي رفع آن بايد هر دو كليد، در اين متد قيد شوند:

var activity1 = db.ActivityTypes.Find(4, 1);

ترتيب آن‌ها هم بر اساس ترتيبي كه در كلاس ActivityTypeConfig، ذكر شده است، مشخص مي‌گردد. بنابراين در اين مثال، اولين پارامتر متد Find، به ActivityID اشاره مي‌كند و دومين پارامتر به UserId.


بررسي نحوه تعريف نگاشت جداول خود ارجاع دهنده (Self Referencing Entity)

سناريوهاي كاربردي بسياري را جهت جداول خود ارجاع دهنده مي‌توان متصور شد و عموما تمام آن‌ها براي مدل سازي اطلاعات چند سطحي كاربرد دارند. براي مثال يك كارمند را درنظر بگيريد. مدير اين شخص هم يك كارمند است. مسئول اين مدير هم يك كارمند است و الي آخر. نمونه ديگر آن، طراحي منوهاي چند سطحي هستند و يا يك مشتري را درنظر بگيريد. مشتري ديگري كه توسط اين مشتري معرفي شده است نيز يك مشتري است. اين مشتري نيز مي‌تواند يك مشتري ديگر را به شما معرفي كند و اين سلسله مراتب به همين ترتيب مي‌تواند ادامه پيدا كند.
در طراحي بانك‌هاي اطلاعاتي، براي ايجاد يك چنين جداولي، يك كليد خارجي را كه به كليد اصلي همان جدول اشاره مي‌كند، ايجاد خواهند كرد؛ اما در EF Code first چطور؟

using System.Collections.Generic;

namespace EF_Sample04.Models
{
    public class Employee
    {
        public int Id { set; get; }
        public string FirstName { get; set; }
        public string LastName { get; set; }

        //public int? ManagerID { get; set; }
        public virtual Employee Manager { get; set; }       
    }
}

در اين كلاس، خاصيت Manager داراي ارجاعي است به همان كلاس؛ يعني يك كارمند مي‌تواند مسئول كارمند ديگري باشد. براي تعريف نگاشت‌ اين كلاس به بانك اطلاعاتي مي‌توان از روش زير استفاده كرد:

using System.Data.Entity.ModelConfiguration;
using EF_Sample04.Models;

namespace EF_Sample04.Mappings
{
    public class EmployeeConfig : EntityTypeConfiguration<Employee>
    {
        public EmployeeConfig()
        {
            this.HasOptional(x => x.Manager)
                .WithMany()
                //.HasForeignKey(x => x.ManagerID)
                .WillCascadeOnDelete(false);
        }
    }
}

با توجه به اينكه يك كارمند مي‌تواند مسئولي نداشته باشد (خودش مدير ارشد است)، به كمك متد HasOptional مشخص كرده‌ايم كه فيلد Manager_Id را كه مي‌خواهي به اين كلاس اضافه كني بايد نال پذير باشد. توسط متد WithMany طرف ديگر رابطه مشخص شده است.
اگر نياز بود فيلد Manager_Id اضافه شده نام ديگري داشته باشد، يك خاصيت nullable مانند ManagerID را كه در كلاس Employee مشاهده مي‌كنيد،‌ اضافه نمائيد. سپس در طرف تعاريف نگاشت‌ها به كمك متد HasForeignKey، بايد صريحا عنوان كرد كه اين خاصيت، همان كليد خارجي است. از اين نكته در ساير حالات تعاريف نگاشت‌ها نيز مي‌توان استفاده كرد، خصوصا اگر از يك بانك اطلاعاتي موجود قرار است استفاده شود و از نام‌هاي ديگري بجز نام‌هاي پيش فرض EF استفاده كرده است.




مثال‌هاي اين سري رو از اين آدرس هم مي‌تونيد دريافت كنيد: (^)