بررسي تعاريف نگاشتها به كمك متاديتا در EF Code first
در قسمت قبل مروري سطحي داشتيم بر امكانات مهياي جهت تعاريف نگاشتها در EF Code first. در اين قسمت، حالت استفاده از متاديتا يا همان data annotations را با جزئيات بيشتري بررسي خواهيم كرد.
براي اين منظور پروژه كنسول جديدي را آغاز نمائيد. همچنين به كمك NuGet، ارجاعات لازم را به اسمبلي EF، اضافه كنيد. در ادامه مدلهاي زير را به پروژه اضافه نمائيد؛ يك شخص كه تعدادي پروژه منتسب ميتواند داشته باشد:
using System; using System.Collections.Generic; namespace EF_Sample02.Models { public class User { public int Id { set; get; } public DateTime AddDate { set; get; } public string Name { set; get; } public string LastName { set; get; } public string Email { set; get; } public string Description { set; get; } public byte[] Photo { set; get; } public IList<Project> Projects { set; get; } } }
using System; namespace EF_Sample02.Models { public class Project { public int Id { set; get; } public DateTime AddDate { set; get; } public string Title { set; get; } public string Description { set; get; } public virtual User User { set; get; } } }
به خاصيت public virtual User User در كلاس Project اصطلاحا Navigation property هم گفته ميشود.
دو كلاس زير را نيز جهت تعريف كلاس Context كه بيانگر كلاسهاي شركت كننده در تشكيل بانك اطلاعاتي هستند و همچنين كلاس آغاز كننده بانك اطلاعاتي سفارشي را به همراه تعدادي ركورد پيش فرض مشخص ميكنند، به پروژه اضافه نمائيد.
using System; using System.Collections.Generic; using System.Data.Entity; using EF_Sample02.Models; namespace EF_Sample02 { public class Sample2Context : DbContext { public DbSet<User> Users { set; get; } public DbSet<Project> Projects { set; get; } } public class Sample2DbInitializer : DropCreateDatabaseAlways<Sample2Context> { protected override void Seed(Sample2Context context) { context.Users.Add(new User { AddDate = DateTime.Now, Name = "Vahid", LastName = "N.", Email = "name@site.com", Description = "-", Projects = new List<Project> { new Project { Title = "Project 1", AddDate = DateTime.Now.AddDays(-10), Description = "..." } } }); base.Seed(context); } } }
به علاوه در فايل كانفيگ برنامه، تنظيمات رشته اتصالي را نيز اضافه نمائيد:
<connectionStrings> <add name="Sample2Context" connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true" providerName="System.Data.SqlClient" /> </connectionStrings>
همانطور كه ملاحظه ميكنيد، در اينجا name به نام كلاس مشتق شده از DbContext اشاره ميكند (يكي از قراردادهاي توكار EF Code first است).
يك نكته:
مرسوم است كلاسهاي مدل را در يك class library جداگانه اضافه كنند به نام DomainClasses و كلاسهاي مرتبط با DbContext را در پروژه class library ديگري به نام DataLayer. هيچكدام از اين پروژهها نيازي به فايل كانفيگ و تنظيمات رشته اتصالي ندارند؛ زيرا اطلاعات لازم را از فايل كانفيگ پروژه اصلي كه اين دو پروژه class library را به خود الحاق كرده، دريافت ميكنند. دو پروژه class library اضافه شده تنها بايد ارجاعاتي را به اسمبليهاي EF و data annotations داشته باشند.
در ادامه به كمك متد Database.SetInitializer كه در قسمت دوم به بررسي آن پرداختيم و با استفاده از كلاس سفارشي Sample2DbInitializer فوق، نسبت به ايجاد يك بانك اطلاعاتي خالي تشكيل شده بر اساس تعاريف كلاسهاي دومين پروژه، اقدام خواهيم كرد:
using System; using System.Data.Entity; namespace EF_Sample02 { class Program { static void Main(string[] args) { Database.SetInitializer(new Sample2DbInitializer()); using (var db = new Sample2Context()) { var project1 = db.Projects.Find(1); Console.WriteLine(project1.Title); } } } }
تا زمانيكه وهلهاي از Sample2Context ساخته نشود و همچنين يك كوئري نيز به بانك اطلاعاتي ارسال نگردد، Sample2DbInitializer در عمل فراخواني نخواهد شد.
ساختار بانك اطلاعاتي پيش فرض تشكيل شده نيز مطابق اسكريپت زير است:
CREATE TABLE [dbo].[Users]( [Id] [int] IDENTITY(1,1) NOT NULL, [AddDate] [datetime] NOT NULL, [Name] [nvarchar](max) NULL, [LastName] [nvarchar](max) NULL, [Email] [nvarchar](max) NULL, [Description] [nvarchar](max) NULL, [Photo] [varbinary](max) NULL, CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY]
CREATE TABLE [dbo].[Projects]( [Id] [int] IDENTITY(1,1) NOT NULL, [AddDate] [datetime] NOT NULL, [Title] [nvarchar](max) NULL, [Description] [nvarchar](max) NULL, [User_Id] [int] NULL, CONSTRAINT [PK_Projects] PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] ) ON [PRIMARY] GO ALTER TABLE [dbo].[Projects] WITH CHECK ADD CONSTRAINT [FK_Projects_Users_User_Id] FOREIGN KEY([User_Id]) REFERENCES [dbo].[Users] ([Id]) GO ALTER TABLE [dbo].[Projects] CHECK CONSTRAINT [FK_Projects_Users_User_Id] GO
توضيحاتي در مورد ساختار فوق، جهت يادآوري مباحث دو قسمت قبل:
- خواصي با نام Id تبديل به primary key و identity field شدهاند.
- نام جداول، همان نام خواص تعريف شده در كلاس Context است.
- تمام رشتهها به nvarchar از نوع max نگاشت شدهاند و null پذير ميباشند.
- خاصيت تصوير كه با آرايهاي از بايتها تعريف شده به varbinary از نوع max نگاشت شده است.
- بر اساس ارتباط بين كلاسها فيلد User_Id در جدول Projects اضافه شده است كه توسط قيدي به نام FK_Projects_Users_User_Id، جهت تعريف كليد خارجي عمل ميكند. اين نام گذاري پيش فرض هم بر اساس نام خواص در دو كلاس انجام ميشود.
- schema پيش فرض بكارگرفته شده، dbo است.
- null پذيري پيش فرض فيلدها بر اساس اصول زبان مورد استفاده تعيين شده است. براي مثال در سي شارپ، نوع int نال پذير نيست يا نوع DateTime نيز به همين ترتيب يك value type است. بنابراين در اينجا اين دو نوع به صورت not null تعريف شدهاند (صرفنظر از اينكه در SQL Server هر دو نوع ياد شده، null پذير هم ميتوانند باشند). بديهي است امكان تعريف nullable types نيز وجود دارد.
مروري بر انواع متاديتاي قابل استفاده در EF Code first
1) Key
همانطور كه ملاحظه كرديد اگر نام خاصيتي Id يا ClassName+Id باشد، به صورت خودكار به عنوان primary key جدول، مورد استفاده قرار خواهد گرفت. اين يك قرارداد توكار است.
اگر يك چنين خاصيتي با نامهاي ذكر شده در كلاس وجود نداشته باشد، ميتوان با مزين سازي خاصيتي مفروض با ويژگي Key كه در فضاي نام System.ComponentModel.DataAnnotations قرار دارد، آنرا به عنوان Primary key معرفي نمود. براي مثال:
public class Project { [Key] public int ThisIsMyPrimaryKey { set; get; }
و ضمنا بايد دقت داشت كه حين كار با ORMs فرقي نميكند EF باشد يا ساير فريم وركهاي ديگر، داشتن يك key جهت عملكرد صحيح فريم ورك، ضروري است. بر اساس يك Key است كه Entity معنا پيدا ميكند.
2) Required
ويژگي Required كه در فضاي نام System.ComponentModel.DataAnnotations تعريف شده است، سبب خواهد شد يك خاصيت به صورت not null در بانك اطلاعاتي تعريف شود. همچنين در مباحث اعتبارسنجي برنامه، پيش از ارسال اطلاعات به سرور نيز نقش خواهد داشت. در صورت نال بودن خاصيتي كه با ويژگي Required مزين شده است، يك استثناي اعتبارسنجي پيش از ذخيره سازي اطلاعات در بانك اطلاعاتي صادر ميگردد. اين ويژگي علاوه بر EF Code first در ASP.NET MVC نيز به نحو يكساني تاثيرگذار است.
3) MaxLength و MinLength
اين دو ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارند (اما در اسمبلي EntityFramework.dll تعريف شدهاند و جزو اسمبلي پايه System.ComponentModel.DataAnnotations.dll نيستند). در ذيل نمونهاي از تعريف اينها را مشاهده ميكنيد. همچنين بايد درنظر داشت كه روش ديگر تعريف متاديتا، تركيب آنها در يك سطر نيز ميباشد. يعني الزامي ندارد در هر سطر يك متاديتا را تعريف كرد:
[MaxLength(50, ErrorMessage = "حداكثر 50 حرف"), MinLength(4, ErrorMessage = "حداقل 4 حرف")] public string Title { set; get; }
ويژگي MaxLength بر روي طول فيلد تعريف شده در بانك اطلاعاتي تاثير دارد. براي مثال در اينجا فيلد Title از نوع nvarchar با طول 30 تعريف خواهد شد.
ويژگي MinLength در بانك اطلاعاتي معنايي ندارد.
هر دوي اين ويژگيها در پروسه اعتبار سنجي اطلاعات مدل دريافتي تاثير دارند. براي مثال در اينجا اگر طول عنوان كمتر از 4 حرف باشد، يك استثناي اعتبارسنجي صادر خواهد شد.
ويژگي ديگري نيز به نام StringLength وجود دارد كه جهت تعيين حداكثر طول رشتهها به كار ميرود. اين ويژگي سازگاري بيشتر با ASP.NET MVC دارد از اين جهت كه Client side validation آنرا نيز فعال ميكند.
4) Table و Column
اين دو ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارند، اما در اسمبلي EntityFramework.dll تعريف شدهاند. بنابراين اگر تعاريف مدلهاي شما در پروژه Class library جداگانهاي قراردارند، نياز خواهد بود تا ارجاعي را به اسمبلي EntityFramework.dll نيز داشته باشند.
اگر از نام پيش فرض جداول تشكيل شده خرسند نيستيد، ويژگي Table را بر روي يك كلاس قرار داده و نام ديگري را تعريف كنيد. همچنين اگر Schema كاربري رشته اتصالي به بانك اطلاعاتي شما dbo نيست، بايد آنرا در اينجا صريحا ذكر كنيد تا كوئريهاي تشكيل شده به درستي بر روي بانك اطلاعاتي اجرا گردند:
[Table("tblProject", Schema="guest")] public class Project
توسط ويژگي Column سه خاصيت يك فيلد بانك اطلاعاتي را ميتوان تعيين كرد:
[Column("DateStarted", Order = 4, TypeName = "date")] public DateTime AddDate { set; get; }
به صورت پيش فرض، خاصيت فوق با همين نام AddDate در بانك اطلاعاتي ظاهر ميگردد. اگر براي مثال قرار است از يك بانك اطلاعاتي قديمي استفاده شود يا قرار نيست از شيوه نامگذاري خواص در سي شارپ در يك بانك اطلاعاتي پيروي شود، توسط ويژگي Column ميتوان اين تعاريف را سفارشي نمود.
توسط پارامتر Order آن كه از صفر شروع ميشود، ترتيب قرارگيري فيلدها در حين تشكيل يك جدول مشخص ميگردد.
اگر نياز است نوع فيلد تشكيل شده را نيز سفارشي سازي نمائيد، ميتوان از پارامتر TypeName استفاده كرد. براي مثال در اينجا علاقمنديم از نوع date مهيا در SQL Server 2008 استفاده كنيم و نه از نوع datetime پيش فرض آن.
نكتهاي در مورد Order:
Order پيش فرض تمام خواصي كه قرار است به بانك اطلاعاتي نگاشت شوند، به int.MaxValue تنظيم شدهاند. به اين معنا كه تنظيم فوق با Order=4 سبب خواهد شد تا اين فيلد، پيش از تمام فيلدهاي ديگر قرار گيرد. بنابراين نياز است Order اولين خاصيت تعريف شده را به صفر تنظيم نمود. (البته اگر واقعا نياز به تنظيم دستي Order داشتيد)
نكاتي در مورد تنظيمات ارث بري در حالت استفاده از متاديتا:
حداقل سه حالت ارث بري را در EF code first ميتوان تعريف و مديريت كرد:
الف) Table per Hierarchy - TPH
حالت پيش فرض است. نيازي به هيچگونه تنظيمي ندارد. معناي آن اين است كه «لطفا تمام اطلاعات كلاسهايي را كه از هم ارث بري كردهاند در يك جدول بانك اطلاعاتي قرار بده». فرض كنيد يك كلاس پايه شخص را داريد كه كلاسهاي بازيكن و مربي از آن ارث بري ميكنند. زمانيكه كلاس پايه شخص توسط DbSet در كلاس مشتق شده از DbContext در معرض استفاده EF قرار ميگيرد، بدون نياز به هيچ تنظيمي، تمام اين سه كلاس، تبديل به يك جدول شخص در بانك اطلاعاتي خواهند شد. يعني يك table به ازاي سلسله مراتبي (Hierarchy) كه تعريف شده.
ب) Table per Type - TPT
به اين معنا است كه به ازاي هر نوع، بايد يك جدول تشكيل شود. به عبارتي در مثال قبل، يك جدول براي شخص، يك جدول براي مربي و يك جدول براي بازيكن تشكيل خواهد شد. دو جدول مربي و بازيكن با يك كليد خارجي به جدول شخص مرتبط ميشوند. تنها تنظيمي كه در اينجا نياز است، قرار دادن ويژگي Table بر روي نام كلاسهاي بازيكن و مربي است. به اين ترتيب حالت پيش فرض الف (TPH) اعمال نخواهد شد.
ج) Table per Concrete Type - TPC
در اين حالت فقط دو جدول براي بازيكن و مربي تشكيل ميشوند و جدولي براي شخص تشكيل نخواهد شد. خواص كلاس شخص، در هر دو جدول مربي و بازيكن به صورت جداگانهاي تكرار خواهد شد. تنظيم اين مورد نياز به استفاده از Fluent API دارد.
توضيحات بيشتر اين موارد به همراه مثال، موكول خواهد شد به مباحث استفاده از Fluent API كه براي تعريف تنظيمات پيشرفته نگاشتها طراحي شده است. استفاده از متاديتا تنها قسمت كوچكي از تواناييهاي Fluent API را شامل ميشود.
5) ConcurrencyCheck و Timestamp
هر دوي اين ويژگيها در فضاي نام System.ComponentModel.DataAnnotations و اسمبلي به همين نام تعريف شدهاند.
در EF Code first دو راه براي مديريت مسايل همزماني وجود دارد:
[ConcurrencyCheck] public string Name { set; get; } [Timestamp] public byte[] RowVersion { set; get; }
زمانيكه از ويژگي ConcurrencyCheck استفاده ميشود، تغيير خاصي در سمت بانك اطلاعاتي صورت نخواهد گرفت، اما در برنامه، كوئريهاي update و delete ايي كه توسط EF صادر ميشوند، اينبار اندكي متفاوت خواهند بود. براي مثال برنامه جاري را به نحو زير تغيير دهيد:
using System; using System.Data.Entity; namespace EF_Sample02 { class Program { static void Main(string[] args) { Database.SetInitializer(new Sample2DbInitializer()); using (var db = new Sample2Context()) { //update var user = db.Users.Find(1); user.Name = "User name 1"; db.SaveChanges(); } } } }
متد Find بر اساس primary key عمل ميكند. به اين ترتيب، اول ركورد يافت شده و سپس نام آن تغيير كرده و در ادامه، اطلاعات ذخيره خواهند شد.
اكنون اگر توسط SQL Server Profiler كوئري update حاصل را بررسي كنيم، به نحو زير خواهد بود:
exec sp_executesql N'update [dbo].[Users] set [Name] = @0 where (([Id] = @1) and ([Name] = @2)) ',N'@0 nvarchar(max) ,@1 int,@2 nvarchar(max) ',@0=N'User name 1',@1=1,@2=N'Vahid'
همانطور كه ملاحظه ميكنيد، براي به روز رساني فقط از primary key جهت يافتن ركورد استفاده نكرده، بلكه فيلد Name را نيز دخالت داده است. از اين جهت كه مطمئن شود در اين بين، ركوردي كه در حال به روز رساني آن هستيم، توسط كاربر ديگري در شبكه تغيير نكرده باشد و اگر در اين بين تغييري رخ داده باشد، يك استثناء صادر خواهد شد.
همين رفتار در مورد delete نيز وجود دارد:
//delete var user = db.Users.Find(1); db.Users.Remove(user); db.SaveChanges();
exec sp_executesql N'delete [dbo].[Users] where (([Id] = @0) and ([Name] = @1))',N'@0 int,@1 nvarchar(max) ',@0=1,@1=N'Vahid'
در اينجا نيز به علت مزين بودن خاصيت Name به ويژگي ConcurrencyCheck، فقط همان ركوردي كه يافت شده بايد حذف شود و نه نمونه تغيير يافته آن توسط كاربري ديگر در شبكه.
البته در اين مثال شايد اين پروسه تنها چند ميلي ثانيه به نظر برسد. اما در برنامهاي با رابط كاربري، شخصي ممكن است اطلاعات يك ركورد را در يك صفحه دريافت كرده و 5 دقيقه بعد بر روي دكمه save كليك كند. در اين بين ممكن است شخص ديگري در شبكه همين ركورد را تغيير داده باشد. بنابراين اطلاعاتي را كه شخص مشاهده ميكند، فاقد اعتبار شدهاند.
ConcurrencyCheck را بر روي هر فيلدي ميتوان بكاربرد، اما ويژگي Timestamp كاربرد مشخص و محدودي دارد. بايد به خاصيتي از نوع byte array اعمال شود (كه نمونهاي از آنرا در بالا در خاصيت public byte[] RowVersion مشاهده نموديد). علاوه بر آن، اين ويژگي بر روي بانك اطلاعاتي نيز تاثير دارد (نوع فيلد را در SQL Server تبديل به timestamp ميكند و نه از نوع varbinary مانند فيلد تصوير). SQL Server با اين نوع فيلد به خوبي آشنا است و قابليت مقدار دهي خودكار آنرا دارد. بنابراين نيازي نيست در حين تشكيل اشياء در برنامه، قيد شود.
پس از آن، اين فيلد مقدار دهي شده به صورت خودكار توسط بانك اطلاعاتي، در تمام updateها و deleteهاي EF Code first حضور خواهد داشت:
exec sp_executesql N'delete [dbo].[Users] where ((([Id] = @0) and ([Name] = @1)) and ([RowVersion] = @2))',N'@0 int,@1 nvarchar(max) , @2 binary(8)',@0=1,@1=N'Vahid',@2=0x00000000000007D1
از اين جهت كه اطمينان حاصل شود، واقعا مشغول به روز رساني يا حذف ركوردي هستيم كه در ابتداي عمليات از بانك اطلاعاتي دريافت كردهايم. اگر در اين بين RowVesrion تغيير كرده باشد، يعني كاربر ديگري در شبكه اين ركورد را تغيير داده و ما در حال حاضر مشغول به كار با ركوردي غيرمعتبر هستيم.
بنابراين استفاده از Timestamp را ميتوان به عنوان يكي از best practices طراحي برنامههاي چند كاربره ASP.NET درنظر داشت.
6) NotMapped و DatabaseGenerated
اين دو ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارند، اما در اسمبلي EntityFramework.dll تعريف شدهاند.
به كمك ويژگي DatabaseGenerated، مشخص خواهيم كرد كه اين فيلد قرار است توسط بانك اطلاعاتي توليد شود. براي مثال خواصي از نوع public int Id به صورت خودكار به فيلدهايي از نوع identity كه توسط بانك اطلاعاتي توليد ميشوند، نگاشت خواهند شد و نيازي نيست تا به صورت صريح از ويژگي DatabaseGenerated جهت مزين سازي آنها كمك گرفت. البته اگر علاقمند نيستيد كه primary key شما از نوع identity باشد، ميتوانيد از گزينه DatabaseGeneratedOption.None استفاده نمائيد:
[DatabaseGenerated(DatabaseGeneratedOption.None)] public int Id { set; get; }
DatabaseGeneratedOption در اينجا يك enum است كه به نحو زير تعريف شده است:
public enum DatabaseGeneratedOption { None = 0, Identity = 1, Computed = 2 }
تا اينجا حالتهاي None و Identity آن، بحث شدند.
در SQL Server امكان تعريف فيلدهاي محاسباتي و Computed با T-SQL نويسي نيز وجود دارد. اين نوع فيلدها در هربار insert يا update يك ركورد، به صورت خودكار توسط بانك اطلاعاتي مقدار دهي ميشوند. بنابراين اگر قرار است خاصيتي به اين نوع فيلدها در SQL Server نگاشت شود، ميتوان از گزينه DatabaseGeneratedOption.Computed استفاده كرد.
يا اگر براي فيلدي در بانك اطلاعاتي default value تعريف كردهايد، مثلا براي فيلد date متد getdate توكار SQL Server را به عنوان پيش فرض درنظر گرفتهايد و قرار هم نيست توسط برنامه مقدار دهي شود، باز هم ميتوان آنرا از نوع DatabaseGeneratedOption.Computed تعريف كرد.
البته بايد درنظر داشت كه اگر خاصيت DateTime تعريف شده در اينجا به همين نحو بكاربرده شود، اگر مقداري براي آن در حين تعريف يك وهله جديد از كلاس User دركدهاي برنامه درنظر گرفته نشود، يك مقدار پيش فرض حداقل به آن انتساب داده خواهد شد (چون value type است). بنابراين نياز است اين خاصيت را از نوع nullable تعريف كرد (public DateTime? AddDate).
همچنين اگر يك خاصيت محاسباتي در كلاسي به صورت ReadOnly تعريف شده است (توسط كدهاي مثلا سي شارپ يا وي بي):
[NotMapped] public string FullName { get { return Name + " " + LastName; } }
بديهي است نيازي نيست تا آنرا به يك فيلد بانك اطلاعاتي نگاشت كرد. اين نوع خواص را با ويژگي NotMapped ميتوان مزين كرد.
همچنين بايد دقت داشت در اين حالت، از اين نوع خواص ديگر نميتوان در كوئريهاي EF استفاده كرد. چون نهايتا اين كوئريها قرار هستند به عبارات SQL ترجمه شوند و چنين فيلدي در جدول بانك اطلاعاتي وجود ندارد. البته بديهي است امكان تهيه كوئري LINQ to Objects (كوئري از اطلاعات درون حافظه) هميشه مهيا است و اهميتي ندارد كه اين خاصيت درون بانك اطلاعاتي معادلي دارد يا خير.
7) ComplexType
ComplexType يا Component mapping مربوط به حالتي است كه شما يك سري خواص را در يك كلاس تعريف ميكنيد، اما قصد نداريد اينها واقعا تبديل به يك جدول مجزا (به همراه كليد خارجي) در بانك اطلاعاتي شوند. ميخواهيد اين خواص دقيقا در همان جدول اصلي كنار مابقي خواص قرار گيرند؛ اما در طرف كدهاي ما به شكل يك كلاس مجزا تعريف و مديريت شوند.
يك مثال:
كلاس زير را به همراه ويژگي ComplexType به برنامه مطلب جاري اضافه نمائيد:
using System.ComponentModel.DataAnnotations; namespace EF_Sample02.Models { [ComplexType] public class InterestComponent { [MaxLength(450, ErrorMessage = "حداكثر 450 حرف")] public string Interest1 { get; set; } [MaxLength(450, ErrorMessage = "حداكثر 450 حرف")] public string Interest2 { get; set; } } }
سپس خاصيت زير را نيز به كلاس User اضافه كنيد:
public InterestComponent Interests { set; get; }
همانطور كه ملاحظه ميكنيد كلاس InterestComponent فاقد Id است؛ بنابراين هدف از آن تعريف يك Entity نيست و قرار هم نيست در كلاس مشتق شده از DbContext تعريف شود. از آن صرفا جهت نظم بخشيدن به يك سري خاصيت مرتبط و همخانواده استفاده شده است (مثلا آدرس يك، آدرس 2، تا آدرس 10 يك شخص، يا تلفن يك تلفن 2 يا موبايل 10 يك شخص).
اكنون اگر پروژه را اجرا نمائيم، ساختار جدول كاربر به نحو زير تغيير خواهد كرد:
CREATE TABLE [dbo].[Users]( ---... [Interests_Interest1] [nvarchar](450) NULL, [Interests_Interest2] [nvarchar](450) NULL, ---...
در اينجا خواص كلاس InterestComponent، داخل همان كلاس User تعريف شدهاند و نه در يك جدول مجزا. تنها در سمت كدهاي ما است كه مديريت آنها منطقيتر شدهاند.
يك نكته:
يكي از الگوهايي كه حين تشكيل مدلهاي برنامه عموما مورد استفاده قرار ميگيرد، null object pattern نام دارد. براي مثال:
namespace EF_Sample02.Models { public class User { public InterestComponent Interests { set; get; } public User() { Interests = new InterestComponent(); } } }
در اينجا در سازنده كلاس User، به خاصيت Interests وهلهاي از كلاس InterestComponent نسبت داده شده است. به اين ترتيب ديگر در كدهاي برنامه مدام نيازي نخواهد بود تا بررسي شود كه آيا Interests نال است يا خير. همچنين استفاده از اين الگو حين كار با يك ComplexType ضروري است؛ زيرا EF امكان ثبت ركورد جاري را در صورت نال بودن خاصيت Interests (صرفنظر از اينكه خواص آن مقدار دهي شدهاند يا خير) نخواهد داد.
8) ForeignKey
اين ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارد، اما در اسمبلي EntityFramework.dll تعريف شدهاست.
اگر از قراردادهاي پيش فرض نامگذاري كليدهاي خارجي در EF Code first خرسند نيستيد، ميتوانيد توسط ويژگي ForeignKey، نامگذاري مورد نظر خود را اعمال نمائيد. بايد دقت داشت كه ويژگي ForeignKey را بايد به يك Reference property اعمال كرد. همچنين در اين حالت، كليد خارجي را با يك value type نيز ميتوان نمايش داد:
[ForeignKey("FK_User_Id")] public virtual User User { set; get; } public int FK_User_Id { set; get; }
در اينجا فيلد اضافي دوم FK_User_Id به جدول Project اضافه نخواهد شد (چون توسط ويژگي ForeignKey تعريف شده است و فقط يكبار تعريف ميشود). اما در اين حالت نيز وجود Reference property ضروري است.
9) InverseProperty
اين ويژگي نيز در فضاي نام System.ComponentModel.DataAnnotations قرار دارد، اما در اسمبلي EntityFramework.dll تعريف شدهاست.
از ويژگي InverseProperty براي تعريف روابط دو طرفه استفاده ميشود.
براي مثال دو كلاس زير را درنظر بگيريد:
public class Book { public int ID {get; set;} public string Title {get; set;} [InverseProperty("Books")] public Author Author {get; set;} } public class Author { public int ID {get; set;} public string Name {get; set;} [InverseProperty("Author")] public virtual ICollection<Book> Books {get; set;} }
اين دو كلاس همانند كلاسهاي User و Project فوق هستند. ذكر ويژگي InverseProperty براي مشخص سازي ارتباطات بين اين دو غيرضروري است و قراردادهاي توكار EF Code first يك چنين مواردي را به خوبي مديريت ميكنند.
اما اكنون مثال زير را درنظر بگيريد:
public class Book { public int ID {get; set;} public string Title {get; set;} public Author FirstAuthor {get; set;} public Author SecondAuthor {get; set;} } public class Author { public int ID {get; set;} public string Name {get; set;} public virtual ICollection<Book> BooksAsFirstAuthor {get; set;} public virtual ICollection<Book> BooksAsSecondAuthor {get; set;} }
اين مثال ويژهاي است از كتابخانهاي كه كتابهاي آن، تنها توسط دو نويسنده نوشته شدهاند. اگر برنامه را بر اساس اين دو كلاس اجرا كنيم، EF Code first قادر نخواهد بود تشخيص دهد، روابط كدام به كدام هستند و در جدول Books چهار كليد خارجي را ايجاد ميكند. براي مديريت اين مساله و تعين ابتدا و انتهاي روابط ميتوان از ويژگي InverseProperty كمك گرفت:
public class Book { public int ID {get; set;} public string Title {get; set;} [InverseProperty("BooksAsFirstAuthor")] public Author FirstAuthor {get; set;} [InverseProperty("BooksAsSecondAuthor")] public Author SecondAuthor {get; set;} } public class Author { public int ID {get; set;} public string Name {get; set;} [InverseProperty("FirstAuthor")] public virtual ICollection<Book> BooksAsFirstAuthor {get; set;} [InverseProperty("SecondAuthor")] public virtual ICollection<Book> BooksAsSecondAuthor {get; set;} }
اينبار اگر برنامه را اجرا كنيم، بين اين دو جدول تنها دو رابطه تشكيل خواهد شد و نه چهار رابطه؛ چون EF اكنون ميداند كه ابتدا و انتهاي روابط كجا است. همچنين ذكر ويژگي InverseProperty در يك سر رابطه كفايت ميكند و نيازي به ذكر آن در طرف دوم نيست.