دوره آموزشی Entity FrameWork Core - قسمت دوازدهم
خوب EF می تواند سلسله مراتبی(hierarchy) از NET type. ها را به پایگاه داده نگاشت(map) کند. این به شما امکان میدهد تا موجودیتهای داتنت خود را به صورت معمول با استفاده از انواع پایه(base) و مشتق شده(derived) در کد، را ایجاد کند. در این پست نگاشت وراثت را در Context یک پایگاه داده رابطه ای بررسی خواهیم کرد.
طبق قرارداد، EF به طور خودکار انواع پایه یا مشتق شده را اسکن نمی کند. این بدان معنی است که اگر
می خواهید یک CLR Type در سلسله مراتب شما نگاشت شود، باید به صراحت آن نوع را در مدل خود مشخص کنید. به عنوان مثال، مشخص کردن نوع پایه یک سلسله مراتب باعث نمی شود که EF Core به طور ضمنی همه زیرشاخه های آن را شامل شود.
نمونه زیر یک DbSet برای Blog و زیر کلاس RssBlog آن را نشان می دهد. اگر Blog دارای زیر کلاس دیگری باشد، در مدل گنجانده نمی شود.
internal class MyContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<RssBlog> RssBlogs { get; set; } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } } public class RssBlog : Blog { public string RssUrl { get; set; } }
در صورت لزوم، هنگام استفاده از TPH mapping، ستون های پایگاه داده به طور خودکار nallable می شوند. به عنوان مثال، ستون RssUrl ـnullable است زیرا نمونه های معمولی Blog آن ویژگی را ندارند.
دو راه برای انجام نگاشت ارث بری در EF Core وجود دارد:
به طور پیش فرض، EF Core یک inheritance hierarchy از NET type. ها را به یک جدول واحد در پایگاه داده نگاشت می کند تا داده ها را برای همه انواع در سلسله مراتب ذخیره کند، و از ستون متمایزی با نام "Discriminator" بعنوان تفکیک کننده موجودیت ها استفاده می شود تا مشخص کند که هر ردیف کدام
Entity Type را نشان می دهد، این به عنوان نگاشت یا mapping جدول به ازای سلسله مراتب (TPH) شناخته
می شود.( یک جدول واحد برای همه کلاس ها در سلسله مراتب وجود دارد.)
از EF Core 5، میتوان هر نوع داتنت(NET types.) را در یک inheritance hierarchy به جدول دیگری نگاشت کرد، و این به عنوان نگاشت Table-per-Type (TPT) شناخته میشود. با این قابلیت، EF Core یک جدول پایه در پایگاه داده ایجاد می کند و یک جدول خاص برای هر جدول مشتق شده ایجاد می کند.(یک جدول به ازای هر کلاس در سلسله مراتب وجود دارد. این از EF Core 5 و بالاتر موجود است.)
فکر می کنم یک مقدار تعاریف پیچیده شد بیایید با مثال آنها را بررسی کنیم :
نگاشت Table-per-Hierarchy (TPH) :
public class Person { public int Id { get; set; } public string Name { get; set; } } public class User : Person { public string UserName { get; set; } } public class Client : Person { public string Email { get; set; } }
در ادامه کلاس DBContext ما به این صورت خواهد بود :
public class ApplicationDBContext : DbContext { public ApplicationDBContext(DbContextOptions<ApplicationDBContext> options) : base(options) { } public DbSet<Person> People { get; set; } public DbSet<Client> Clients { get; set; } public DbSet<User> Users { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { } }
پس از انجام migration، این اسکریپت SQL است که توسط EF Core تولید می شود:
migrationBuilder.CreateTable( name: "People", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Name = table.Column<string>(type: "nvarchar(max)", nullable: false), Discriminator = table.Column<string>(type: "nvarchar(max)", nullable: false), Email = table.Column<string>(type: "nvarchar(max)", nullable: true), UserName = table.Column<string>(type: "nvarchar(max)", nullable: true) }, constraints: table => { table.PrimaryKey("PK_People", x => x.Id); });
یک جدول واحد ایجاد می شود که حاوی فیلدی به نام «Discriminator» به معنای مشخص کننده است و با این ستون می توان بین انواع مختلف(در اینجا User و Client) تمایز قائل شد:
حالا بیایید یک کوئری LINQ ایجاد کنیم تا همه کاربران(Users) را برگرداند:
using var db = new ApplicationContext(); var users = db.Users.ToList();
پرس و جوی SQL که برای آن ایجاد می شود به صورت زیر خواهد بود:
SELECT [p].[Id], [p].[Discriminator], [p].[Name], [p].[UserName] FROM [People] AS [p] WHERE [p].[Discriminator] = N'User'
توجه داشته باشید که با رویکرد TPH، امکان داشتن یک پراپرتی required در موجودیت های مشتق شده (Client یا User) وجود ندارد، زیرا این یک جدول مشترک است و به همین دلیل، تمام ویژگی های موجود در موجودیت های مشتق شده باید اجازه مقادیر null را (nuallable)بدهند. برای مثال، اگر تنظیماتی را با استفاده از DataAnnotations برای پراپرتی Email در کلاس Client تنظیم کنیم، در MIGRATION نادیده گرفته خواهند شد.
public class Client : Person { [Required] public string Email { get; set; } }
نکات تکمیلی Table-per-Hierarchy :
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>() .HasDiscriminator<string>("person_type") .HasValue<Person>("person_base") .HasValue<Client>("person_client"); }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{ modelBuilder.Entity<Blog>() .Property("Discriminator") .HasMaxLength(200); }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasDiscriminator() .IsComplete(false); }
public class MyContext : DbContext { public DbSet<BlogBase> Blogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Url) .HasColumnName("Url"); modelBuilder.Entity<RssBlog>() .Property(b => b.Url) .HasColumnName("Url"); } } public abstract class BlogBase { public int BlogId { get; set; } } public class Blog : BlogBase { public string Url { get; set; } } public class RssBlog : BlogBase { public string Url { get; set; } }
توجه داشته باشید در ارائهدهندگان پایگاه داده رابطهای(Relational database providers)، مانند SQL Server، در حالت shared columns و انجام پرس و جو هنگام استفاده از cast، بهطور خودکار از تفکیککننده(discriminator) استفاده نمیکنند در نتیجه باید یک فیلتر را به صورت دستی بر روی discriminator اعمال کنید.
نگاشت Table-per-Type (TPT) :
حالا بیایید مثالی را با استفاده از نگاشت TPT ببینیم. برای این مثال، ما از همان سه موجودیت استفاده خواهیم کرد: Person (کلاس پایه)، user و client (کلاس های مشتق شده).
دو راه برای ایجاد نگاشت TPT وجود دارد و با این پیکربندی، EF Core یک جدول برای هر موجودیت ایجاد
می کند. یکی از راه های انجام آن استفاده از DataAnnotation در نام کلاس ها است، به عنوان مثال:
[Table("People")] public class Person { public int Id { get; set; } public string Name { get; set; } } [Table("Users")] public class User : Person { public string UserName { get; set; } } [Table("Clients")] public class Client : Person { public string Email { get; set; } }
راه دیگر پیکربندی جداول با استفاده از Fluent API است:
public class PersonConfiguration : IEntityTypeConfiguration<Person> { public void Configure(EntityTypeBuilder<Person> builder) { builder.ToTable("People"); } } public class ClientConfiguration : IEntityTypeConfiguration<Client> { public void Configure(EntityTypeBuilder<Client> builder) { builder.ToTable("Clients"); } } public class UserConfiguration : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { builder.ToTable("Users"); } }
هنگامی که پیکربندی توسط Fluent API انجام می شود، همچنین لازم است که پیکربندی زیر را نیز اعمال کنید.، به این ترتیب EF Core نگاشت را برای تمام پیکربندی هایی که IEntityTypeConfiguration را پیاده سازی
می کنند انجام می دهد:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDBContext).Assembly); }
پس از انجام migration، این اسکریپت SQL است که توسط EF Core تولید می شود:
migrationBuilder.CreateTable( name: "People", columns: table => new { Id = table.Column<int>(type: "int", nullable: false) .Annotation("SqlServer:Identity", "1, 1"), Name = table.Column<string>(type: "nvarchar(max)", nullable: false) }, constraints: table => { table.PrimaryKey("PK_People", x => x.Id); }); migrationBuilder.CreateTable( name: "Clients", columns: table => new { Id = table.Column<int>(type: "int", nullable: false), Email = table.Column<string>(type: "nvarchar(max)", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Clients", x => x.Id); table.ForeignKey( name: "FK_Clients_People_Id", column: x => x.Id, principalTable: "People", principalColumn: "Id"); }); migrationBuilder.CreateTable( name: "Users", columns: table => new { Id = table.Column<int>(type: "int", nullable: false), UserName = table.Column<string>(type: "nvarchar(max)", nullable: false) }, constraints: table => { table.PrimaryKey("PK_Users", x => x.Id); table.ForeignKey( name: "FK_Users_People_Id", column: x => x.Id, principalTable: "People", principalColumn: "Id"); });
یا در نهایت این اسکرییپت :
CREATE TABLE[People] ( [Id] int NOT NULL IDENTITY, [Name] nvarchar(max) NULL, CONSTRAINT[PK_People] PRIMARY KEY([Id]) ); CREATE TABLE[Clients] ( [Id] int NOT NULL, [Email] nvarchar(max) NULL, CONSTRAINT[PK_Clients] PRIMARY KEY([Id]), CONSTRAINT[FK_Clients_People_Id] FOREIGN KEY([Id]) REFERENCES[People]([Id]) ON DELETE NO ACTION ); CREATE TABLE[Users] ( [Id] int NOT NULL, [UserName] nvarchar(max) NULL, CONSTRAINT[PK_Users] PRIMARY KEY([Id]), CONSTRAINT[FK_Users_People_Id] FOREIGN KEY([Id]) REFERENCES[People]([Id]) ON DELETE NO ACTION );
توجه داشته باشید که اکنون ما سه جدول داریم، هر کدام برای هر موجودیت. هر جدول دارای ویژگی های خاص خود است و یک کلید خارجی در جدول Clients و Users با اشاره به جدول People وجود دارد.
حالا بیایید یک کوئری LINQ ایجاد کنیم تا همه کاربران(Users) را برگرداند:
using var db = new ApplicationContext(); var users = db.Users.ToList();
پرس و جوی SQL که برای آن ایجاد می شود به صورت زیر خواهد بود:
SELECT [p].[Id], [p].[Name], [u].[UserName] FROM [People] AS [p] INNER JOIN [Users] AS [u] ON [p].[Id] = [u].[Id]
توجه داشته باشید که اکنون در این پرس و جوی SQL، جستجو از جدول افراد با یک INNER JOIN با جدول کاربران انجام می شود. این نتیجه این پرس و جو است:
با TPT موجودیتها جداول خود را در پایگاه داده خواهند داشت (برای هر موجودیت مشتق شده جدول
جداگانه ای ایجاد میشود) و آنها یک جدول را به اشتراک نگذاشته اند بنابراین اکنون ممکن است یک پراپرتی required در موجودیت های مشتق شده (Client یا User) در نظر گرفت.
دو تفاوت کلیدی بین TPT و TPH وجود دارد:
نگاشت TPH بالقوه در پرس و جو عملکرد بهتری دارد
بیشتر بخوانید : دوره آموزشی Entity FrameWork Core - قسمت چهاردهم
بیشتر بخوانید : دوره آموزشی Entity FrameWork Core
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core