تنظيمات ارث بري كلاسها در EF Code first
بانكهاي اطلاعاتي مبتني بر SQL، تنها روابطي از نوع «has a» يا «داراي» را پشتيباني ميكنند؛ اما در دنياي شيءگرا روابطي مانند «is a» يا «هست» نيز قابل تعريف هستند. براي توضيحات بيشتر به مدلهاي زير دقت نمائيد:
using System; namespace EF_Sample05.DomainClasses.Models { public abstract class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime DateOfBirth { get; set; } } }
namespace EF_Sample05.DomainClasses.Models { public class Coach : Person { public string TeamName { set; get; } } }
namespace EF_Sample05.DomainClasses.Models { public class Player : Person { public int Number { get; set; } public string Description { get; set; } } }
در اين مدلها كه بر اساس ارث بري از كلاس شخص، تهيه شدهاند؛ بازيكن، يك شخص است. مربي نيز يك شخص است؛ و به اين ترتيب خوانده ميشوند:
Coach "is a" Person Player "is a" Person
در EF Code first سه روش جهت كار با اين نوع كلاسها و كلا ارث بري وجود دارد كه در ادامه به آنها خواهيم پرداخت:
الف) Table per Hierarchy يا TPH
همانطور كه از نام آن نيز پيدا است، كل سلسله مراتبي را كه توسط ارث بري تعريف شده است، تبديل به يك جدول در بانك اطلاعاتي ميكند. اين حالت، شيوه برخورد پيش فرض EF Code first با ارث بري كلاسها است و نياز به هيچگونه تنظيم خاصي ندارد.
براي آزمايش اين مساله، كلاس Context را به نحو زير تعريف نمائيد و سپس اجازه دهيد تا EF بانك اطلاعاتي معادل آنرا توليد كند:
using System.Data.Entity; using EF_Sample05.DomainClasses.Models; namespace EF_Sample05.DataLayer.Context { public class Sample05Context : DbContext { public DbSet<Person> People { set; get; } } }
ساختار جدول توليد شده آن همانند تصوير زير است:
همانطور كه ملاحظه ميكنيد، تمام كلاسهاي مشتق شده از كلاس شخص را تبديل به يك جدول كرده است؛ به علاوه يك فيلد جديد را هم به نام Discriminator به اين جدول اضافه نموده است. براي درك بهتر عملكرد اين فيلد، چند ركورد را توسط برنامه به بانك اطلاعاتي اضافه ميكنيم. حاصل آن به شكل زير خواهد بود:
از فيلد Discriminator جهت ثبت نام كلاسهاي متناظر با هر ركورد، استفاده شده است. به اين ترتيب EF حين كار با اشياء دقيقا ميداند كه چگونه بايد خواص متناظر با كلاسهاي مختلف را مقدار دهي كند.
به علاوه اگر به ساختار جدول تهيه شده دقت كنيد، مشخص است كه در حالت TPH، نياز است فيلدهاي متناظر با كلاسهاي مشتق شده از كلاس پايه، همگي null پذير باشند. براي نمونه فيلد Number كه از نوع int تعريف شده، در سمت بانك اطلاعاتي نال پذير تعريف شده است.
و براي كوئري نوشتن در اين حالت ميتوان از متد الحاقي OfType جهت فيلتر كردن اطلاعات بر اساس كلاسي خاص، كمك گرفت:
db.People.OfType<Coach>().FirstOrDefault(x => x.LastName == "Coach L1")
سفارشي سازي نحوه نگاشت TPH
همانطور كه عنوان شد، TPH نياز به تنظيمات خاصي ندارد و حالت پيش فرض است؛ اما براي مثال ميتوان بر روي مقادير و نوع ستون Discriminator توليدي، كنترل داشت. براي اين منظور بايد از Fluent API به نحو زير استفاده كرد:
using System.Data.Entity.ModelConfiguration; using EF_Sample05.DomainClasses.Models; namespace EF_Sample05.DataLayer.Mappings { public class CoachConfig : EntityTypeConfiguration<Coach> { public CoachConfig() { // For TPH this.Map(m => m.Requires(discriminator: "PersonType").HasValue(1)); } } }
using System.Data.Entity.ModelConfiguration; using EF_Sample05.DomainClasses.Models; namespace EF_Sample05.DataLayer.Mappings { public class PlayerConfig : EntityTypeConfiguration<Player> { public PlayerConfig() { // For TPH this.Map(m => m.Requires(discriminator: "PersonType").HasValue(2)); } } }
در اينجا توسط متد Map، نام فيلد discriminator به PersonType تغيير كرده. همچنين چون مقدار پيش فرض تعيين شده توسط متد HasValue عددي است، نوع اين فيلد در سمت بانك اطلاعاتي به int null تغيير ميكند.
ب) Table per Type يا TPT
در حالت TPT، به ازاي هر كلاس موجود در سلسله مراتب تعيين شده، يك جدول در سمت بانك اطلاعاتي تشكيل ميگردد.
در جداول متناظر با Sub classes، تنها همان فيلدهايي وجود خواهند داشت كه در كلاسهاي هم نام وجود دارد و فيلدهاي كلاس پايه در آنها ذكر نخواهد گرديد. همچنين اين جداول داراي يك Primary key نيز خواهند بود (كه دقيقا همان كليد اصلي جدول پايه است كه به آن Shared primary key هم گفته ميشود). اين كليد اصلي، به عنوان كليد خارجي اشاره كننده به كلاس يا جدول پايه نيز تنظيم ميگردد:
براي تنظيم اين نوع ارث بري، تنها كافي است ويژگي Table را بر روي Sub classes قرار داد:
using System.ComponentModel.DataAnnotations; namespace EF_Sample05.DomainClasses.Models { [Table("Coaches")] public class Coach : Person { public string TeamName { set; get; } } }
using System.ComponentModel.DataAnnotations; namespace EF_Sample05.DomainClasses.Models { [Table("Players")] public class Player : Person { public int Number { get; set; } public string Description { get; set; } } }
يا اگر حالت Fluent API را ترجيح ميدهيد، همانطور كه در قسمتهاي قبل نيز ذكر شد، معادل ويژگي Table در اينجا، متد ToTable است.
ج) Table per Concrete type يا TPC
در تعاريف ارث بري كه تاكنون بررسي كرديم، مرسوم است كلاس پايه را از نوع abstract تعريف كنند. به اين ترتيب هدف اصلي، Sub classes تعريف شده خواهند بود؛ چون نميتوان مستقيما وهلهاي را از كلاس abstract تعريف شده ايجاد كرد.
در حالت TPC، به ازاي هر sub class غير abstract، يك جدول ايجاد ميشود. هر جدول نيز حاوي فيلدهاي كلاس پايه ميباشد (برخلاف حالت TPT كه جداول متناظر با كلاسهاي مشتق شده، تنها حاوي همان خواص و فيلدهاي كلاسهاي متناظر بودند و نه بيشتر). به اين ترتيب عملا جداول تشكيل شده در بانك اطلاعاتي، از وجود ارث بري در سمت كدهاي ما بيخبر خواهند بود.
براي پياده سازي TPC نياز است از Fluent API استفاده شود:
using System.ComponentModel.DataAnnotations; using System.Data.Entity.ModelConfiguration; using EF_Sample05.DomainClasses.Models; namespace EF_Sample05.DataLayer.Mappings { public class PersonConfig : EntityTypeConfiguration<Person> { public PersonConfig() { // for TPC this.Property(x => x.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.None); } } }
using System.Data.Entity.ModelConfiguration; using EF_Sample05.DomainClasses.Models; namespace EF_Sample05.DataLayer.Mappings { public class CoachConfig : EntityTypeConfiguration<Coach> { public CoachConfig() { // For TPH //this.Map(m => m.Requires(discriminator: "PersonType").HasValue(1)); // for TPT //this.ToTable("Coaches"); //for TPC this.Map(m => { m.MapInheritedProperties(); m.ToTable("Coaches"); }); } } }
using System.Data.Entity.ModelConfiguration; using EF_Sample05.DomainClasses.Models; namespace EF_Sample05.DataLayer.Mappings { public class PlayerConfig : EntityTypeConfiguration<Player> { public PlayerConfig() { // For TPH //this.Map(m => m.Requires(discriminator: "PersonType").HasValue(2)); // for TPT //this.ToTable("Players"); //for TPC this.Map(m => { m.MapInheritedProperties(); m.ToTable("Players"); }); } } }
ابتدا نوع فيلد Id از حالت Identity خارج شده است. اين مورد جهت كار با TPC ضروري است در غيراينصورت EF هنگام ثبت، به مشكل بر ميخورد، از اين لحاظ كه براي دو شيء، به يك Id خواهد رسيد و امكان ثبت را نخواهد داد. بنابراين در يك چنين حالتي استفاده از نوع Guid براي تعريف primary key شايد بهتر باشد. بديهي است در اين حالت بايد Id را به صورت دستي مقدار دهي نمود.
در ادامه توسط متد MapInheritedProperties، به همان مقصود لحاظ كردن تمام فيلدهاي ارث بري شده در جدول حاصل، خواهيم رسيد. همچنين نام جداول متناظر نيز ذكر گرديده است.
سؤال : از اين بين، بهتر است از كداميك استفاده شود؟
- براي حالتهاي ساده از TPH استفاده كنيد. براي مثال يك بانك اطلاعاتي قديمي داريد كه هر جدول آن 200 تا يا شايد بيشتر فيلد دارد! امكان تغيير طراحي آن هم وجود ندارد. براي اينكه بتوان به حس بهتري حين كاركردن با اين نوع سيستمهاي قديمي رسيد، ميشود از تركيب TPH و ComplexTypes (كه در قسمتهاي قبل در مورد آن بحث شد) براي مديريت بهتر اين نوع جداول در سمت كدهاي برنامه استفاده كرد.
- اگر علاقمند به استفاده از روابط پليمرفيك هستيد ( براي مثال در كلاسي ديگر، ارجاعي به كلاس پايه Person وجود دارد) و sub classes داراي تعداد فيلدهاي كمي هستند، از TPH استفاده كنيد.
- اگر تعداد فيلدهاي sub classes زياد است و بسيار بيشتر است از كلاس پايه، از روش TPT استفاده كنيد.
- اگر عمق ارث بري و تعداد سطوح تعريف شده بالا است، بهتر است از TPC استفاده كنيد. حالت TPT از join استفاده ميكند و حالت TPC از union براي تشكيل كوئريها كمك خواهد گرفت