حسین احمدی
بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

کاملترین آموزش Entity Framework رایگان

اگر شغل شما برنامه نویسی هست، حتماً واژه ORM و در کنارش Entity Framework رو شنیدید. يکي از مهمترين ابزار هايي که در نوشتن برنامه هاي Data-Centric استفاده ميشه، ابزار هاي ORM يا Object Relational Mapping هست. اين ابزار ها اين قابليت رو به شما ميدهند که داخل برنامه از بانک اطلاعاتي و جداول داخل اون به صورت Object ها استفاده کنيد. ابزار هاي زيادي در اين مورد وجود دارند که معروف ترين اون ها عبارتند از NHibernate و Entity Framework. در اين مقاله قصد دارم تا در مورد ويژگي Code-First در Entity Framework بنويسم.براي استفاده از اين مقاله ابزار هاي زير مورد نياز هستند:

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران
  1. Microsoft Visual Studio
  2. Microsoft SQL Server

براي شروع ابتدا Script زير رو داخل Sql Server اجرا کنيد:

CREATE DATABASE Sales;
GO
CREATE TABLE Sales..ProductCategories
(
    ID INT NOT NULL IDENTITY PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL
);
GO
CREATE TABLE Sales..Products
(
    ID INT NOT NULL IDENTITY PRIMARY KEY,
    CategoryID INT NOT NULL REFERENCES Sales..ProductCategories(ID),
    Name NVARCHAR(100) NOT NULL
);
GO


بعد از اجراي Script بالا، پروژه اي داخل Visual Studio ايجاد کرده و سپس به Entity Framework يک Reference بدهيد. (براي استفاده از EntityFramework Code First مي تونيد دستور Install-Package EntityFramewokr را که يک دستور NuGet هست اجرا کنيد) حالا بايد داخل پروژه جداول بانک رو به يک سري کلاس Map کنيم. براي اين کار ابتدا کلاس هاي زير را ايجاد کنيد:

public class ProductCategory
{
    public int ID { get; set }
    public string Name { get; set; }
    public virtual ICollection Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public ProductCategory Category { get; set; }
    public int CategoryID { get; set; }
    public string Name { get; set; }
}

public class SalesContext : DbContext
{
    public DbSet ProductCategories { get; set; }
    public DbSet Products { get; set; }
}

همانطور که مشاهده مي کنيد، ما سه کلاس ايجاد کرديم، کلاس هاي ProductCategory، Prduct و SalesContext. دو کلاس اول که کلاس هاي عادي هستند، کلاس آخري يعني SalesContext کار اصلي رو براي ما انجام ميده. يعني وظيفه ارتباط بين کلاس ها و جداول بانک اطلاعاتي رو به عهده خواهد داشت.

داخل کلاس Context ما دو Property ايجاد کرديم. اين Property ها در حقيقت همون جداول ما هستند. حال بايد Connection String بانک رو براي کلاس Context تعريف کنيم. براي اين کار فايل app.config را باز کرده و به صورت زير تغيير مي دهيم: 

    
        
    


دقت کنيد که نام ConnectionString بايد با نام Context يکي باشد. کل کاري که بايد انجام مي داديم همين بود، حالا مي تونيم از اين کلاس ها داخل برنامه استفاده کنيم. براي مثال، براي اضافه کردن يک ProductCategory بايد به صورت زير عمل کنيم:

SalesContext context = new SalesContext();
context.ProductCategories.Add(new ProductCategory() { Name = "Category1" });
context.SaveChanges(); 


با اين کار يک Product Category جديد به بانک ما اضافه خواهد شد. حالا فرض کنيد که مي خواهيم به يک Category يک Product اضافه کنيم، براي اين کار يک سري تغييرات بايد انجام شود. کلاس DbContext يک Virtual Method دارد با نام OnModelCreating که با Override کردن اين متد مي توان عمليات Custom Mapping را انجام داد. در اينجا براي شناساندن Relation بين ProductCategory و Product بايد کد زير را در کلاس SalesContext اضافه کرد:

public override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity()
        .HasMany(category => category.Products)
        .WithRequired(product => product.Category)
        .WithForeignKey(product => product.CategoryID);
}


با اين کار به کلاس SalesContext مي فهمانيم که کلاس ProductCategory چندين Product دارد که خود Product يک عدد Category داد که بايد حتما" وارد شود که رابطه بين اين دو با کليد خارجي CategoryID مشخص مي شود. (دقت کنيد که در اينجا رابطه ما **-1 يا همان یک به چند است). حالا با اجراي قطعه کد زير مي توان به يک Category يک Product جديد اضافه کرد:

SalesContext context = new SalesContext();
var category = context.ProductCategories.Find(1);
category.Products.Add(new Product() { Name = "Product1" });
context.SaveChanges();


دقت کنيد که داخل کلاس ProductCategory، ليست Products بايد از نوع virtual تعريف شود تا Entity Framework بتواند ليست Product ها را هنگام خواندن Category بارگذاري کند.در قسمت بعدي در مورد نحوه نوشتن کوئري ها و همچنين استفاده از Fluent Api توضيحات بيشتري خواهم داد.

کلاس DbContext و DbSet

در قسمت دوم از سری آموزش Entity Framework در مورد کلاس DbContext و DbSet که دو کلاس اصلی در تعریف مدل بانک اطلاعاتی در سمت برنامه سی شارپ ما هستند صحبت می کنیم.

مروری بر کلاس DbContext و DbSet

همانطوری که در بخش قبلی مطرح کردیم، کلاس Context ما باید از کلاس DbContext ارث بری کند. کلاس DbContext در حقیقت یک Container برای Entity های ما هست که وظیفه شناسایی و تعریف Mapping ها را به صورت خودکار انجام می دهد.

در حقیقت کلاس DbContext یک Wrapper بر روی ObjectContext که خیلی از ویژگی های این کلاس را به صورت خیلی ساده تر در اختیار ما قرار می دهد. البته خیلی از ویژگی های ObjectContext به صورت مستیقیم قابل دسترس نیست و باید به صورت غیر مستقیم به اونها دسترسی پیدا کرد.

کلاس هایی که از DbContext مشتق می شوند معمولا" دارای یک یا چند DbSet هستند. کلاس DbSet که یک کلاس Generic هست، اینترفیس IQueryable را پیاده سازی می کند تا بتوان از قابلیت LINQ در این کلاس برای نوشتن کوئری ها استفاده کرد.

هنگام ساختن یک شئ از کلاس مشتق شده از DbContext، تمامی DbSet ها شناسایی و پردازش می شوند تا Entity ها به جداول بانک اطلاعاتی Map شوند. عملیات Mapping به صورت خودکار انجام می شود که می توان از طریق متد OnModelCreating که یک متد Virtual هست، تغییرات دلخواه در مورد Mapping را اعمال کرد. مدل زیر را در نظر بگیرید:

public class Customer
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class OrdersContext : DbContext
{
    public DbSet Customers { get; set; }
}


هنگام ساختن یک شئ از OrdersContext، کلیه DbSet ها پردازش شده و به بانک اطلاعاتی Map می شوند. برای مثال کلاس Customer به جدول Customers در بانک اطلاعاتی Map خواهد شد. وظیفه تشخیص اسم جمع Customer که Customers می شود، به عهده خود DbContext است.

همچنین، هر Property از نوع Primitive Types داخل Entity ها معادل یک Column در جداول بانک اطلاعاتی است. هنگام پردازش Property ها، ID به صورت خودکار به عنوان کلید اولیه در نظر گرفته خواهد شد. اما برای کلاس Context جهت ارتباط با بانک مورد نظر ما، باید یک Connection String مشخص کنیم، چند راه برای این کار وجود دارد که ساده ترین روش، قرار دادن یک Connection String داخل app.config یا web.config با نام کلاس Context هست. برای مثال:

  
    
  


یکی از قابلیت های Code-First، امکان ایجاد بانک بر اساس مدل در زمان اجرا است. برای ایجاد بانک می توان از دستور زیر استفاده کرد:

OrdersContext context = new OrdersContext();
context.Database.Create();


با اجرای دستور بالا، بانک Orders بر اساس مدل ایجاد خواهد شد. حالا اگر بخواهیم بانک در صورتی ایجاد شود که قبلا" ایجاد نشده باشد به جای متد Create، از متد CreateIfNotExists استفاده می کنیم. کلاس Context یک Property با نام Database دارد که به وسیله این Property می توانیم به یک سری قابلیت ها، مانند دسترسی به DbConnection جاری، یا ایجاد بانک اطلاعاتی دسترسی داشته باشیم.

متد Find در کلاس DbSet

این دستور به ما این امکان را می دهد تا بر اساس کلید اولیه در داخل بانک جستجو کنیم:

var customer = context.Customers.Find(12);


دستور بالا معادل دستور زیر است:

var customer = context.Customers.FirstOrDefault(c => c.CustomerID == 12);

متد Add و Remove در کلاس DbSet

برای اضافه کردن یک Entity، از متد Add استفاده می کنیم:

var newCustomer = new Customer();
newCustomer.FirstName = "Reza";
newCustomer.LastName = "Mohammadi";
context.Customers.Add(newCustomer);
context.SaveChanges();


باید دقت کنیم که صدا زدن متد SaveChange در کلاس Context جهت ثبت تغییرات در بانک اطلاعاتی الزامی می باشد. اگر بخواهیم یک Entity را از بانک حذف کنیم از متد Remove استفاده می کنیم. متد Remove یک پارامتر از نوع Entity مورد نظر را دریافت کرده و آن را حذف می کند:

var customer1 = context.Customers.Find(12);
var customer2 = context.Customers.First(c => c.FirstName == "Reza");
context.Customers.Remove(customer1);
context.Customers.Remove(customer2);
context.SaveChanges();


با اجرای دستور بالا Entity های مورد نظر از بانک اطلاعاتی حذف خواهند شد.

قابلیت DQE در اجرای Query ها

یکی از قابلیت های LINQ قابلی DQE یا Deffered Query Execution هست، این معنی که تا زمانی که نتایج درخواست نشوند، کوئری اجرا نخواهد شد، برای مثال دستور زیر را در نظر بگیرید:

var customers = context.Customers.Where(c => c.FirstName.Contains("reza")).OrderBy(c => c.FirstName);


با اجرای دستور بالا، اگر با پنجره Watch متغیر Customer را نگاه کنیم، عبارت زیر را خواهیم دید:

SELECT 
[Extent1].[CustomerID] AS [CustomerID], 
[Extent1].[FirstName] AS [FirstName], 
[Extent1].[LastName] AS [LastName]
FROM [dbo].[Customers] AS [Extent1]
WHERE [Extent1].[FirstName] LIKE N'%reza%'
ORDER BY [Extent1].[FirstName] ASC


این دستور SQL عبارت تولید شده دستور #C بالاست. حالا اگر متد ()customers.ToList را اجرا کنیم، تازه دستور SQL ایجاد شده بالا، اجرا خواهد شد و نتیجه به صورت لیستی از Customer ها برگردانده خواهد شد. در بخش بعدی چگونی تغییر Mapping را بوسیله Attribute ها بررسی خواهیم کرد.

تعریف Map با Attribute ها

در ادامه آموزش Entity Framework به نحوه تعریف Map با استفاده از Attribute ها می پردازیم. همانطور که گفتیم Map به ایجاد رابطه میان کلاس ها و Property کلاس ها و جداول بانک اطلاعاتی می گوییم، برای مثال، خصوصیت FirstName داخل کلاس Customer به ستون FirstName داخل جدول Customers اشاره می کنه.

اما میشه این رابطه ها رو تغییر داد. برای مثال میتونیم بگیم خصوصیت FirstName به ستون Name داخل بانک اشاره کنه. این کار به دو روش امکان پذیره، روش اول استفاده از Attribute ها است، روش دوم استفاده از Fluent API هست که در بخش بعدی در مورد Fluent API ها توضیح خواهیم داد. Attribute هایی که در این بخش بررسی خواهیم کرد به شرح زیر می باشند:

  1. TableAttribute
  2. ColumnAttribute
  3. RequiredAttribute
  4. MaxLengthAttribute
  5. KeyAttribute
  6. DatabaseGeneratedAttribute
  7. NotMappedAttribute


کلیه Attribute های بالا، در فضای نام System.ComponentModel.DataAnnotations قرار دارند.

TableAttribute

کار این Attribute، تغییر جدول Map شده به Entity است. کلاس Customer که در بخش قبلی استفاده کردیم را به خاطر دارید؟

public class Customer
{
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}


به صورت پیش فرض این کلاس به جدول Customers در بانک Map خواهد شد. Entity Framework به صورت پیش فرض اسم جمع Customer را که Customers می شود به عنوان نام جدول انتخاب می کند. با TableAttribute می توانیم این پیش فرض را تغییر دهیم، کافی است که از این Attribute در بالای کلاس استفاده کرده و نام جدول مورد نظر را به آن بدهیم:

[Table("MyTable")]
public class Customer


با قرار دادن Attribute بالا روی کلاس Customer، ما به Entity Framework می گوییم که کلاس جدول معادل کلاس Customer در بانک اطلاعاتی، جدول MyTable است. با استفاده از این Attribute، ما می توانیم Schema جدول را نیز مشخص کنیم:

[Table("MyTable",Schema = "orders")]
public class Customer

ColumnAttribute

وظیفه این Attribute، تغییر Mapping خصوصیات کلاس و ستون های جدول هست. برای مثال، ما می خواهیم خصوصیت FirstName در کلاس Customer به ستون FName در جدول بانک اطلاعاتی متصل شود. برای این کار کافیست این Attribute را بالای خصوصیت مورد نظر بگذاریم:

[Column("FName")]
public string FirstName { get; set; }


همچنین، با استفاده از این Attribute می توانیم نوع ستون جدول را نیز مشخص کنیم، برای مثال، کلیه خصوصیات از نوع string، به عنوان nvarchar در نظر گرفته می شوند، برای تغییر، کافیست نوع داده را از طریق TypeName مشخص کنیم:

[Column("FName",TypeName = "varchar")]
public string FirstName { get; set; }

RequiredAttribute

به صورت پیش فرض، کلیه خصوصیات داخل کلاس که از نوع string هستند، ستون معادل آنها در بانک از نوع Nullable در نظر گرفته می شود، اگر این Attribute بر روی یک خصوصیت قرار داده شود، ستون معادل از نوع Not Null در نظر گرفته خواهد شد.

MaxLengthAttribute

بوسیله این Attribute می توان طول یک ستون را در بانک مشخص کرد، به صورت پیش فرض کلیه خصوصیات string، با طول 255 در نظر گرفته می شوند. با این خصوصیت می توان این طول را تغییر داد:

[Column("FName",TypeName = "varchar")]
[MaxLength(1000)]
public string FirstName { get; set; }


با کد بالا، خصوصیت FirstName به ستون FName از نوع varchar با طول 1000 Map خواهد شد.

KeyAttribute

وقتی شما یک کلاسی را داخل DbContext به عنوان Entity مشخص می کنید، اولین خصوصیتی که دارای کلمه Id باشد یا ترکیبی از نام کلاس و کلمه Id باشد (بزرگی و کوچکی حروف تفاوتی نمی کند)، به عنوان ستون کلید در نظر گرفته خواهد شد. برای مثال CustomerId، ID، ProductId یا هر اسم دیگری که داخل آن Id باشد به عنوان کلید در نظر گرفته می شود. این مسئله قابل تغییر است، کافیست بر روی ستون کلید کلاس، این Attribute را قرار دهید تا به عنوان کلید در نظر گرفته شود. دقت کنید که تنها خصوصیاتی که نوع عددی صحیح دارند یا از نوع GUID هستند، می توانند به عنوان کلید انتخاب شوند.

DatabaseGeneratedAttribute

در صورتی که خصوصیت شما به ستونی از نوع Identity یا Computed اشاره می کند، از این Attribute استفاده کنید. وظیفه این Attribute مشخص کردن نحوه برخورد بانک با یک ستون از یک جدول است.

برای مشخص کردن از نوع Identity:

[DatabaseGenerated(DatabaseGeneratedOption.Identity)]


برای مشخص کردن از نوع Computed:

[DatabaseGenerated(DatabaseGeneratedOption.Computed)]

NotMappedAttribute

بعضی اوقات، داخل کلاس ما یک خصوصیت هست که به هیچ ستونی داخل بانک اشاره نمی کند. برای مثال فرض کنیم که می خواهیم داخل کلاس Customer یک خصوصیت با نام Title داشته باشیم که این ستون معادلی داخل بانک ندارد. برای این که این مسئله را به Entity Framework بفهمانیم، این Attribute را بر روی خصوصیت مورد نظر می گذاریم.

در صورت عدم وجود این کار، خطا رخ خواهد داد با این مضمون که ستون معادل برای Title در جدول وجود ندارد. در صورتی که خصوصیت ما به صورت Get Only باشد، یعنی فقط بخش get برای این کار نوشته شده باشد، دیگر نیازی به استفاده از این Attribute نیست.در بخش بعدی مقاله در مورد استفاده از Fluent API و ایجاد رابطه ها صحبت خواهیم کرد.

تعریف Map با Fluent API

در این بخش قصد دارم تا با Fluent API در Entity Framework و نحوه تعریف Mapping بوسیله آن آشنا شویم. ابتدا بهتر است در مورد Fluent-API بیشتر توضیح بدهم. بر اساس توضیح سایت Wikipedia، مفهوم Fluent API یا Fluent Interface به معنی پیاده سازی API شی گرا می باشد که قابلیت خوانایی آن را افزایش می دهد.این مفهوم دقیقا" در مورد Entity Framework Code-First نیز صدق می کند. بدین معنی که شما می توانید دستور های Mapping را به صورتی بنویسید که قابلیت خوانایی و درک آن بالاتر برود.برای تعریف Mapping با کمک Fluent API می توان به دو صورت عمل کرد:

  1. نوشتن دستورات در متد OnModelCreating
  2. ایجاد یک کلاس جداگانه برای Map و اضافه کردن آن به کلاس Context


برای استفاده مورد اول، متد OnModelCreating را که یک متد virtual هست، در کلاس Context باید Override کنیم، پارامتر این متد یک شئ از نوع DbModelBuilder هست. بوسیله متد های این شئ ما می توانیم عملیات Mapping را انجام دهیم. در ادامه به بررسی متد های زیر از شئ DbModelBuilder می پردازیم:

  1. متد Entity
  2. متد ToTable
  3. متد Property
  4. متد HasColumnName
  5. متد HasColumnType
  6. متد HasDatabaseGeneratedOption
  7. متدهای IsRequired و IsOptional
  8. متد IsMaxLength
  9. متد HasMaxLength
  10. متد HasKey
  11. متد Ignore

متد Entity

در حقیقت این متد انتخاب کننده کلاسی هست که می خواهیم عملیات Mapping را برای آن انجام دهیم، نوع جنریک این متد باید از نوع کلاسی باشد که عملیات Mapping برای آن انجام می شود، نوع برگشتی این متد از نوع EntityTypeConfiguration است که بوسیله متد های آن می توانیم عملیات Mapping مربوط به آن Entity را انجام دهیم.

متد ToTable

این متد که در کلاس EntityTypeConfiguration تعریف شده است، تعیین کننده جدول معادل کلاس در بانک اطلاعاتی است:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity().ToTable("MyTable");
}

Attribute معادل: TableAttribute

متد Property

برای تغییر Mapping یک خصوصیت، از این متد استفاده می کنیم، در این حقیقت این متد Selector خصوصیت می باشد و نوع برگشتی آن بر اساس نوع خصوصیت انتخابی فرق می کند. برای مثال با انتخاب یک خصوصیت از نوع رشته، نوع برگشتی این متد از نوع StringPropertyConfigurator یا برای خصوصیات از نوع عددی از نوع PrimitivePropertyConfigurator می باشد. برای مثال، انتخاب خصوصیت FirstName از کلاس Customer:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Entity().Property(customer => customer.FirstName)
}


متد HasColumnName

بوسیله این متد می توان نام ستون معادل یک خصوصیت را بانک اطلاعاتی مشخص کرد، برای مثال، در زیر خصوصیت FirstName به ستون FName و خصوصیت LastName به ستون LName اشاره خواهد کرد:

    modelBuilder.Entity().Property(customer => customer.FirstName).HasColumnName("FName");
    modelBuilder.Entity().Property(customer => customer.LastName).HasColumnName("LName");


Attribute معادل: ColumnAttribute

متد HasColumnName

بوسیله این متد می توان نوع ستون معادل خصوصیت انتخاب شده در بانک اطلاعاتی را مشخص کرد:

modelBuilder.Entity().Property(customer => customer.FirstName).HasColumnType("varchar");


Attribute معادل: ColumnAttribute

متد HasDatabaseGeneratedOption

اگر ستون معادل خصوصیت انتخابی در بانک، از نوع Identity یا Computed باشد، بوسیله این متد می توان این مورد را به Entity Framework شناساند:

modelBuilder.Entity().Property(customer => customer.CustomerID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);


متد های IsRequired و IsOptional

بوسیله این دو متد می توان نوع ستون را که مقدار Null قبول می کند یا خیر مشخص کرد:

  1. IsRequired: ستون مقدار Null قبول نمی کند.
  2. IsOptional: ستون مقدار Null قبول می کند.


    modelBuilder.Entity().Property(customer => customer.FirstName).IsRequired();
    modelBuilder.Entity().Property(customer => customer.LastName).IsOptional();

متد IsMaxLength

این متد تعیین می کند که یک خصوصیت از نوع رشته باید خداکثر طول وارد شده را قبول کند، معادل MAX برای نوع های varchar و nvarchar در بانک اطلاعاتی:

    modelBuilder.Entity().Property(customer => customer.LastName).IsMaxLength();

متد HasMaxLength

بوسیله این متد می توان طول قابل قبول برای یک ستون را مشخص کرد:

modelBuilder.Entity().Property(customer => customer.FirstName).HasMaxLength(2000);


در صورت دادن مقدار null به ورود این متد، هیچ محدودیتی برای طول در نظر گرفته نخواهد شد. توجه کنید که متدهای IsMaxLength و HasMaxLength تنها برای خصوصیات از نوع رشته قابل استفاده هستند.

Attribute معادل: MaxLengthAttribute

متد HasKey

برای تعریف یک خصوصیت به عنوان کلید، می توان از این متد استفاده کرد:

modelBuilder.Entity().HasKey(customer => customer.CustomerID);


Attribute معال: KeyAttribute

متد Ignore

بوسیله این متد می توان گفت که یک خصوصیت به هیچ ستونی در بانک اشاره نمی کند:

modelBuilder.Entity().Ignore(customer => customer.FullName);


Attribute معادل: NotMappedAttribute

استفاده از قابلیت Method Chaining

با استفاده از این قابلیت دیگر نیازی نیست برای هر مورد از تعریف ها برای یک خصوصیت یک خط جدا بنویسیم، به دلیل اینکه مقدار برگشتی اکثر متدها از نوع PropertyConfigurator است، می توان پس از یک دستور، دستور بعدی را نیز نوشت:

modelBuilder.Entity().Property(customer => customer.FirstName)
    .HasColumnName("FName")
    .IsRequired()
    .HasMaxLength(2000)
    .HasColumnType("varchar");


با این روش، در زمان صرفه جویی خواهیم کرد. تا اینجای کار با برخی از متد ها آشنا شدیم، اما ممکن است بعضی وقت ها، Model ما خیلی بزرگ باشد، برای مثال از 100 کلاس مختلف تشکیل شده باشد. در این جور مواقع بهتر است که یک کلاس جداگانه برای تعریف Mapping ها بنوسیم و سپس کلاس Mapping را به کلاس Context اضافه کنیم. برای این کار ابتدا یک کلاس تعریف می کنیم که از کلاس EntityTypeConfiguration مشتق شده باشد، نوع جنریک را از نوع کلاس مورد نظر انتخاب می کنیم، سپس داخل Constructor کلاس دستورات Mapping را می نویسیم:

public class CustomerMap : EntityTypeConfiguration
{
    public CustomerMap()
    {
        HasKey(customer => customer.CustomerID);
        Property(customer => customer.FirstName)
            .HasColumnName("FName")
            .HasColumnType("varchar")
            .IsRequired()
            .HasMaxLength(2000);
        Property(customer => customer.LastName)
            .HasColumnName("LName")
            .IsOptional()
            .IsMaxLength();
    }
}


حالا این کلاس را می توانیم بوسیله دستور زیر به کلاس Context اضافه کنیم:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    modelBuilder.Configurations.Add(new CustomerMap());
}


در بخش بعدی چگونگی تعریف رابطه ها بوسیله Attribute ها و Fluent API را بررسی خواهیم کرد.

انواع رابطه ها (Relation)

در ادامه آموزش Entity Framework در باره  انواع رابطه ها و مفهوم آنها صحبت خواهیم کرد. رابطه ها و استفاده آنها در Entity Framework در دو بخش ارائه خواهند شد. بخش اول به تعریف ساده رابطه ها خواهیم پرداخت، در بخش دوم نحوه تغییر در تعریف رابطه ها بوسیله Fluent API و Attribute را بررسی خواهیم کرد. در ابتدا تعریفی از رابطه را ارائه داده و انواع رابطه و چگونگی استفاده آنها را در Entity Framework بررسی خواهیم خواهیم کرد.

تعریف رابطه ها

به طور کلی وقتی یک موجودیت ما با یک موجودیت دیگر ارتباط داشته باشد، به آن رابطه یا Relation می گویند. برای مثال، در یک سیستم مدیریت فروش، ما یک موجودیت مشتری داریم، هر مشتری می تواند داخل سیستم چندین سفارش ثبت شده داشته باشد. یعنی در سیستم ما، بین موجودیت مشتری و سفارش رابطه وجود دارد.

انواع رابطه

به طور کلی رابطه ها بر چهار نوع می باشند:

  1. رابطه One-To-Many
  2. رابطه Zero Or One-To-Many
  3. رابطه Many-To-Many
  4. رابطه One-To-One

رابطه One-To-Many

این نوع رابطه که رابطه پیک به چند نیز نامیده می شوند به این معنی می باشند که یک موجودیت می تواند یک یا چندین موجودیت دیگر نیز داشته باشد. به مثال مشتری و سفارش برمی گردیم، گفتیم که یک مشتری می تواند داخل سیستم چندین سفارش ثبت شده داشته باشد و هر سفارش متعلق به یک مشتری است. در حقیقت رابطه بین مشتری و سفارش یک رابطه یک به چند است.

رابطه Zero Or One-To-Many

مثال مشتری و سفارش را در نظر بگیرید، رابطه از نوع یک به چند بود، در این رابطه هر سفارش باید یک مشتری داشته باشد، ولی اگر رابطه مشتری و سفارش از نوع Zero or One-To-Many باشد سفارش می تواند بدون مشتری باشد، یعنی هیچ مشتری برای سفارش تعیین نشود.

رابطه Many-To-Many

این رابطه که رابطه چند به چند نیز نام دارد، تعیین می کند که هر دو موجودیت می توانند چندین مورد از موجودیت دیگر را داشته باشند. برای مثال، در یک سیستم ما تعدادی کاربر و تعدادی گروه های کاربری داریم. هر گروه کاربری می تواند چندین گروه کاربری و هر گروه کاربری چندین کاربر داشته باشد. در حقیقت رابطه بین گروه کاربری و کاربر رابطه از نوع چند به چند است.

رابطه One-To-One

این رابطه که رابطه یک به یک نیز نام دارد، تعیین می کند دو موجودیت، تنها یک مورد از موجودیت دیگر را دارند. برای مثال در یک سیستم، ما یک کاربر و یک پروفایل کاربر داریم، هر کاربر متعلق به یک پروفایل و هر پروفایل متعلق به یک کاربر می باشد، یعنی یک کاربر نمی تواند چندین پروفایل داشته باشد.

در این مورد، رابطه بین کاربر و پروفایل از نوع یک به یک است. *این نوع رابطه کاربر زیادی ندارد، زیرا در Entity Framework پیاده سازی این رابطه با Complex Type ها منطقی تر است که در بخش های بعدی در مورد قابلیت Complex Type توضیح خواهیم داد.

در ادامه به نحوه پیاده سازی این رابطه در Entity Framework و همچنین نحوه تعریف جداول بانک اطلاعاتی خواهیم پرداخت. بر بخش اول این تعریف ها از Attribute ها یا Fluent API برای تعریف Mapping استفاده نخواهیم کرد، زیرا Entity Framework به صورت خودکار با پردازش کلاس ها رابطه ها را شناسایی خواهد کرد.

رابطه One-To-Many

مثالی که در این مورد استفاده خواهیم کرد، همان مثال مشتری و سفارش است. موجودیت های مشتری و سفارش را به صورت زیر تعریف می کنیم:

    public class Customer
    {
        public int CustomerID { get; set; }
        public string Name { get; set; }
        public virtual ICollection Orders { get; set; }       
    }

    public class Order
    {
        public int OrderID { get; set; }
        public virtual Customer Customer { get; set; }
        public DateTime Date { get; set; }
    }


همانطور که در کد بالا مشاهده می کنید، در کلاس Customer یک Collection از نوع Order و در کلاس Order یک خاصیت از نوع Customer. مشخص است که یک مشتری چندین سفارش و هر سفارش یک مشتری دارد.

در کد بالا خصوصیت های Orders در کلاس Customer و Customer در کلاس Order از نوع virtual هستند. دلیل این تعریف قابلیت Lazy Loading می باشد که از خصوصیات Entity Framework می باشد که در قسمت های بعدی در مورد این قابلیت توضیح خواهیم داد. فقط باید دقت کرد که در صورت عدم استفاده از virtual در تعریف خصوصیت های مربوط به رابطه ها، دسترسی به اطلاعات امکان پذیر نخواهد بود.

جدول معادل این رابطه به صورت زیر می باشدک

CREATE TABLE [Customers]
(
	[CustomerID] INT IDENTITY NOT NULL PRIMARY KEY,
	[Name] NVARCHAR(250) NOT NULL
);
GO

CREATE TABLE [Orders]
(
	[OrderID] INT IDENTITY NOT NULL PRIMARY KEY,
	[Customer_ID] INT NOT NULL REFERENCES Customers(CustomerID),
	[Date] DATETIME NOT NULL
);
GO

رابطه Zero or One-To-Many

تنها تفاوت این رابطه به رابطه یک به چند نحوه نعریف جدول Orders در بانک اطلاعاتی است:

CREATE TABLE [Orders]
(
	[OrderID] INT IDENTITY NOT NULL PRIMARY KEY,
	[Customer_ID] INT NULL REFERENCES Customers(CustomerID),
	[Date] DATETIME NOT NULL
);
GO


ستون Customer_ID در این جدول از نوع NULL تعریف شده است.

رابطه Many-To-Many

این رابطه مشخص می کند که دو موجودیت می توانند چندین مورد از دیگری را دارا باشند. مثال کاربر و گروه کاربری را بررسی می کنیم. کلاس های زیر را در نظر بگیرید:

public class User
{
    public int UserID { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public virtual ICollection UserGroups { get; set; } 
}

public class UserGroup
{
    public int UserGroupID { get; set; }
    public string GroupName { get; set; }
    public virtual ICollection Users { get; set; } 
}


همانطور که مشاهده می کنید کلاس User دارای یک خصوصیت به نام UserGroups بوده که از نوع مجموعه ای از UserGroup می باشد و همچنین کلاس UserGroup دارای خصوصیتی با نام Users از نوع مجموعه کلاس User می باشد. دقت کنید که هر دو خصوصیت از نوع virtual می باشند. اما نحوه تعریف رابطه چند به چند در بانک اطلاعاتی به چه صورت است؟ برای تعریف این رابطه، نیاز به یک جدول کمکی داریم. تعریف رابطه بالا در بانک اطلاعاتی به صورت زیر می باشد:

CREATE TABLE [Users]
(
	[UserID] INT IDENTITY NOT NULL PRIMARY KEY,
	[Username] NVARCHAR(250) NOT NULL,
	[Password] NVARCHAR(250) NOT NULL
);
GO

CREATE TABLE [UserGroups]
(
	[UserGroupID] INT IDENTITY NOT NULL PRIMARY KEY,
	[GroupName] NVARCHAR(250) NOT NULL
);
GO

CREATE TABLE [UsersUserGroups]
(
	[User_ID] INT NOT NULL REFERENCES [Users](UserID),
	[UserGroup_ID] INT NOT NULL REFERENCES [UserGroups](UserGroupID),
	PRIMARY KEY([User_ID],[UserGroup_ID])
);
GO


جدول سوم با نام UsersUserGoups رابطه چند به چند را بین جدول های Users و UserGroups برقرار می کند.

رابطه One-To-One

ابن نوع رابطه را به دلیل خاص بودن نوع ان به قسمت بعد موکول خواهیم کرد. در قسمت بعدی نحوه تغییر در Mapping بوسیله Fluent API و Attribute ها را بررسی خواهیم کرد.

تعریف رابطه ها با Fluent API

در ادامه این مقالات به بررسی تعریف رابطه با استفاده از Fluent API و Attribute ها خواهیم پرداخت. در هر قسمت ابتدا با Fluent API و سپس با Attribute ها اقدام به تعریف رابطه ها خواهیم کرد.

رابطه One-To-Many و Zero Or One-To-Many

این رابطه از دو طرف قابل تنظیم است. کلاس های قبلی را به یاد بیاورید:

public class Customer
{
    public int CustomerID { get; set; }
    public string Name { get; set; }
    public virtual ICollection Orders { get; set; }
}

public class Order
{
    public int OrderID { get; set; }
    public virtual Customer Customer { get; set; }
    public DateTime Date { get; set; }
}


برای تعریف این رابطه با Fluent API متد OnModelCreating را در کلاس Context مان Override کرده و داخل آن کدهای زیر را می نویسیم:

modelBuilder.Entity()
    .HasMany(customer => customer.Orders)
    .WithRequired(order => order.Customer);


با این کد می گوییم که کلاس Customer دارای چندین Order و کلاس Order دارای یک Customer می باشد. یعنی رابطه ما یک به چند است. این عمل به صورت عکس نیز قابل تعریف است:

modelBuilder.Entity()
    .HasRequired(order => order.Customer)
    .WithMany(customer => customer.Orders);


به متد HasRequired دقت کنید. اینجا چون رابطه ما One-To-Many هست از متد HasRequired استفاده می کنیم. یعنی Customer رای کلاس Order اجباری است. در صورتی که بخواهیم رابطه به صورت Zero Or One-To-Many تعریف شود باید از متد HasOptional استفاده کنیم:

modelBuilder.Entity()
    .HasOptional(order => order.Customer)
    .WithMany(customer => customer.Orders);


اما جدول Orders ما در بانک اطلاعاتی، دارای یک ForeignKey می باشد. به صورت پیش فرض برای رابطه بالا، Entity Framework ستون Customer_Id را به عنوان کلید خارجی در نظر می گیرد. اما فرض کنیم که نام کلید خارجی ما در جدول Orders ستون CustomerID باشد. برای این که ما این ستون را به عنوان کلید خارجی در نظر بگیریم، از متد HasForeignKey استفاده می کنیم. لازمه این کار این است که یک Property با نام CustomerID به کلاس Order اضافه کنیم و سپس اقدام به تعریف رابطه کنیم:

تعریف کلاس Orders:

public class Order
{
    ...
    public int CustomerID { get; set; }
    ...
}


استفاده از متد HasForeignKey:

modelBuilder.Entity()
    .HasRequired(order => order.Customer)
    .WithMany(customer => customer.Orders)
    .HasForeignKey(order => order.CustomerID);


دقت کنید که اگر از متد HasOptional استفاده کردیم باشیم (رابطه ما Zero or One-To-Many باشد)، خصوصیت CustomerID در کلاس Orders باید Nullable تعریف شود:

    public int? CustomerID { get; set; }


حالا، اگر ما نخواهیم خصوصیت CustomerID داخل کلاسمان تعریف شود چه کاری باید انجام دهیم؟ چاره این کار استفاده از Attribute ها است. برای کلید تعیین کلید خارجی از ForeignKey استفاده می کنیم که بر روی خصوصیت Customer در کلاس Orders قرار می گیرد:

    [ForeignKey("CustomerID")]
    public virtual Customer Customer { get; set; }


متد WillCascadeOnDelete: این متد برای این استفاده میشه که تعیین کنیم وقتی یک Customer حذف شد آیا تمام Order های آن نیز حذف شود یا خیر. این متد یک پارامتر boolean میگیرد یا می توان به آن هیچ پارامتری پاس نداد. در صورتی که هیچ پارامتری به آن پاس ندیم، عملیات Cascade انجام می شود، ولی در صورت ارسال مقدار false به این متد، در صورتی که Customer دارای Order ای باشد، عملیات از عملیات حذف جلوگیری خواهد شد:

modelBuilder.Entity()
    .HasOptional(order => order.Customer)
    .WithMany(customer => customer.Orders)
    .WillCascadeOnDelete(true);


*اگر از Fluent API استفاده نکرده و تنها بخواهیم با استفاده از Attribute ها نوع رابطه که One یا Zero-Or-One باشد را تعیین کنیم، اگر هیچ Attribute ای روی خصوصیت Customer نگذاریم، رابطه از نوع Zero-Or-One در نظر گرفته خواهد شد، ولی اگر Required را روی خصوصیت Customer در کلاس Order قرار دهیم، رابطه از نوع One در نظر گرفته می شود.

رابطه Many-To-Many

کد زیر را در نظر بگیرید:

public class User
{
    public int UserID { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public virtual ICollection UserGroups { get; set; }
}

public class UserGroup
{
    public int UserGroupID { get; set; }
    public string GroupName { get; set; }
    public virtual ICollection Users { get; set; }
}


برای رابطه چند به چند داخل متد OnModelCreating کد زیر را می نویسیم:

modelBuilder.Entity()
    .HasMany(user => user.UserGroups)
    .WithMany(userGroup => userGroup.Users);


اما مهمترین دلیل استفاده از Fluent API تغییر نام جدول سول و تعیین نام ستون های آن است، به صورت پیش فرض نام جدول UsersUserGroups، ستون اول UserId و ستون دوم UserGroupId در نظر گرفته خواهد شد که می توان با استفاده از متد Map تغییرات مورد نظر را اعمال کرد. متد Map یک ورودی از نوع Action که پارامتر آن از نوع ManyToManyAssociationMappingConfiguration دارد که سه متد زیر را در اختیار ما قرار می دهد:

  1. ToTable: برای تعیین جدول ManyToMany
  2. MapLeftKey: برای تعیین کلید خارجی موجودیت اول
  3. MapRightKey: برای تعیین کلید خارجی موجودیت دوم


برای کلاس های بالا تعریف زیر را در نظر بگیرید:

modelBuilder.Entity()
    .HasMany(user => user.UserGroups)
    .WithMany(userGroup => userGroup.Users)
    .Map(configuration => configuration.MapLeftKey("UserID").MapLeftKey("UserGroupID").ToTable("UsersAttachedGroups"));


در اینجا نام جدول Many-To-Many را UsersAttachedGroups و ستون های آن را به ترتیب UserID و UserGroupID تعریف کردیم.

  • نکته ای که باید در نظر گرفت استفاده به جا از MapLeftKey و MapRightKey می باشد. به خاطر داشته باشید که همیشه MapLeftKey مربوط به کلید خارجی کلاسی است که به عنوان پارامتر جنریک در متد Entity استفاده شده و MapRightKey برای کلاس مقابل آن است ، می توان کلید های خارجی را با استفاده از ForeignKey Attribute نیز مشخص کرد.

رابطه One-To-One

این رابطه به این صورت است که دو رابطه بین دو جدول بر اساس کلید های اولیه مشخص می شود. برای روشن شدن موضوع کلاس های زیر را در نظر بگیرید:

public class User
{
    public int UserID { get; set; }
    public UserProfile Profile { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}

public class UserProfile
{
    public int UserProfileID { get; set; }
    public User User { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public byte Age { get; set; }
}


کلاس User دارای یک UserProfile و کلاس UserProfile دارای یک User است. در این رابطه که در کلاس بالا مشاهده می کنید، در جدول UserProfile ستون UserID هم به عنوان کلید اولیه و هم به عنوان کلید خارجی در نظر گرفته خواهد شد. برای تعریف این رابطه با Fluent API کد زیر را در متد OnModelCreating بنویسید:

modelBuilder.Entity()
    .HasRequired(user => user.Profile)
    .WithRequiredDependent(profile => profile.User);


اگر بخواهیم خصوصیت Profile برای User اختیاری باشد به صورت زیر می نویسیم:

modelBuilder.Entity()
    .HasRequired(user => user.Profile)
    .WithOptional(profile => profile.User);


متدهای دیگری برای رابطه های One-To-One کاربرد دارند که عبارتند از RequiredPrincipal و RequiredDepend که در بخش های بعدی در مورد این متدها بیشتر توضیح خواهیم داد. همچنین نوع دیگری از رابطه ها وجود دارند با نام Complex Types که توضیحات مربوط به آن را نیز به بخش های بعد موکول می کنیم. موفق و پیروز باشید...


حسین احمدی
حسین احمدی

بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

حسین احمدی ، بنیانگذار TOSINSO ، توسعه دهنده وب و برنامه نویس ، بیش از 12 سال سابقه فعالیت حرفه ای در سطح کلان ، مشاور ، مدیر پروژه و مدرس نهادهای مالی و اعتباری ، تخصص در پلتفرم دات نت و زبان سی شارپ ، طراحی و توسعه وب ، امنیت نرم افزار ، تحلیل سیستم های اطلاعاتی و داده کاوی ...

نظرات