فرشید عزیزی
فرشید عزیزی
خواندن ۱۰ دقیقه·۲ سال پیش

دوره آموزشی Entity FrameWork Core - قسمت سیزدهم


دوره آموزشی Entity FrameWork Core - قسمت دوازدهم

وراثت(Inheritance) :

خوب EF می تواند سلسله مراتبی(hierarchy) از NET type. ها را به پایگاه داده نگاشت(map) کند. این به شما امکان می‌دهد تا موجودیت‌های دات‌نت خود را به صورت معمول با استفاده از انواع پایه(base) و مشتق شده(derived) در کد، را ایجاد کند. در این پست نگاشت وراثت را در Context یک پایگاه داده رابطه ای بررسی خواهیم کرد.

نگاشت سلسله مراتبی Entity type hierarchy mapping :

طبق قرارداد، 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 آن ویژگی را ندارند.

نگاشت Table-per-Type و Table-per-Hierarchy :

دو راه برای انجام نگاشت ارث بری در 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; } }
  • کلاس "Person" کلاس پایه(base) است.
  • کلاس "User" از Person ارث بری می کند.
  • کلاس "Client" نیز از Person ارث بری می کند.

در ادامه کلاس 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: &quotPeople&quot, columns: table => new { Id = table.Column<int>(type: &quotint&quot, nullable: false) .Annotation(&quotSqlServer:Identity&quot, &quot1, 1&quot), Name = table.Column<string>(type: &quotnvarchar(max)&quot, nullable: false), Discriminator = table.Column<string>(type: &quotnvarchar(max)&quot, nullable: false), Email = table.Column<string>(type: &quotnvarchar(max)&quot, nullable: true), UserName = table.Column<string>(type: &quotnvarchar(max)&quot, nullable: true) }, constraints: table => { table.PrimaryKey(&quotPK_People&quot, 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 :

  • می توانید نام و نوع ستون تفکیک کننده (discriminator column) و مقادیری را که برای شناسایی هر نوع در سلسله مراتب استفاده می شود، پیکربندی کنید:
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>() .HasDiscriminator<string>(&quotperson_type&quot) .HasValue<Person>(&quotperson_base&quot) .HasValue<Client>(&quotperson_client&quot); }
  • در مثال های بالا، EF متمایزکننده (discriminator column) را به طور ضمنی به عنوان یک ویژگی سایه(shadow property) بر موجودیت پایه سلسله مراتب اضافه کرد. این ویژگی را می توان مانند هر ویژگی دیگر پیکربندی کرد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{ modelBuilder.Entity<Blog>() .Property(&quotDiscriminator&quot) .HasMaxLength(200); }
  • هنگام پرس و جو برای موجودیت های مشتق شده، که از الگوی TPH استفاده می کنند، EF Core یک گزاره بر ستون تفکیک کننده در پرس و جو اضافه می کند. این فیلتر مطمئن می‌شود که هیچ ردیف اضافی برای انواع sibling type که در نتیجه نیستند دریافت نمی‌کنیم. این گزاره فیلتر برای نوع موجودیت پایه نادیده گرفته می شود زیرا پرس و جو برای موجودیت پایه نتایجی را برای همه موجودیت های سلسله مراتب دریافت می کند. هنگام تحقق نتایج یک پرس و جو، اگر با یک مقدار تفکیک کننده مواجه شدیم که به هیچ نوع موجودیتی در مدل نگاشت نشده است، یک استثنا ایجاد می کنیم زیرا نمی دانیم چگونه نتایج را تحقق بخشیم. این خطا تنها زمانی رخ می دهد که پایگاه داده شما دارای ردیف هایی با مقادیر تفکیک کننده باشد که در مدل EF نگاشت نشده اند. اگر چنین داده‌هایی دارید، می‌توانید نگاشت تفکیک‌کننده در مدل EF Core را به‌عنوان ناقص علامت‌گذاری کنید. فراخوانی IsComplete(false) بر روی پیکربندی تشخیصگر، mapping را ناقص نشان می دهد.
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasDiscriminator() .IsComplete(false); }
  • ستون های مشترک (Shared columns) :
    به طور پیش فرض، زمانی که دو نوع موجودیت sibling entity types در سلسله مراتب دارای یک ویژگی با نام مشابه باشند، آنها به دو ستون جداگانه نگاشت می شوند. با این حال، اگر نوع آنها یکسان باشد، می توان آنها را به یک ستون پایگاه داده نگاشت کرد. مثال زیر کاملا مفهوم را می رساند:
public class MyContext : DbContext { public DbSet<BlogBase> Blogs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Url) .HasColumnName(&quotUrl&quot); modelBuilder.Entity<RssBlog>() .Property(b => b.Url) .HasColumnName(&quotUrl&quot); } } 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(&quotPeople&quot)] public class Person { public int Id { get; set; } public string Name { get; set; } } [Table(&quotUsers&quot)] public class User : Person { public string UserName { get; set; } } [Table(&quotClients&quot)] public class Client : Person { public string Email { get; set; } }

راه دیگر پیکربندی جداول با استفاده از Fluent API است:

public class PersonConfiguration : IEntityTypeConfiguration<Person> { public void Configure(EntityTypeBuilder<Person> builder) { builder.ToTable(&quotPeople&quot); } } public class ClientConfiguration : IEntityTypeConfiguration<Client> { public void Configure(EntityTypeBuilder<Client> builder) { builder.ToTable(&quotClients&quot); } } public class UserConfiguration : IEntityTypeConfiguration<User> { public void Configure(EntityTypeBuilder<User> builder) { builder.ToTable(&quotUsers&quot); } }

هنگامی که پیکربندی توسط Fluent API انجام می شود، همچنین لازم است که پیکربندی زیر را نیز اعمال کنید.، به این ترتیب EF Core نگاشت را برای تمام پیکربندی هایی که IEntityTypeConfiguration را پیاده سازی
می کنند انجام می دهد:

protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDBContext).Assembly); }

پس از انجام migration، این اسکریپت SQL است که توسط EF Core تولید می شود:

migrationBuilder.CreateTable( name: &quotPeople&quot, columns: table => new { Id = table.Column<int>(type: &quotint&quot, nullable: false) .Annotation(&quotSqlServer:Identity&quot, &quot1, 1&quot), Name = table.Column<string>(type: &quotnvarchar(max)&quot, nullable: false) }, constraints: table => { table.PrimaryKey(&quotPK_People&quot, x => x.Id); }); migrationBuilder.CreateTable( name: &quotClients&quot, columns: table => new { Id = table.Column<int>(type: &quotint&quot, nullable: false), Email = table.Column<string>(type: &quotnvarchar(max)&quot, nullable: false) }, constraints: table => { table.PrimaryKey(&quotPK_Clients&quot, x => x.Id); table.ForeignKey( name: &quotFK_Clients_People_Id&quot, column: x => x.Id, principalTable: &quotPeople&quot, principalColumn: &quotId&quot); }); migrationBuilder.CreateTable( name: &quotUsers&quot, columns: table => new { Id = table.Column<int>(type: &quotint&quot, nullable: false), UserName = table.Column<string>(type: &quotnvarchar(max)&quot, nullable: false) }, constraints: table => { table.PrimaryKey(&quotPK_Users&quot, x => x.Id); table.ForeignKey( name: &quotFK_Users_People_Id&quot, column: x => x.Id, principalTable: &quotPeople&quot, principalColumn: &quotId&quot); });

یا در نهایت این اسکرییپت :

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 بالقوه در پرس و جو عملکرد بهتری دارد

  • با TPH، داده ها همه در یک جدول هستند. با TPT، داده ها به چندین جدول تقسیم می شوند که نیاز به اتصال دارد. در تئوری، JOIN شدن چندین جدول عملکرد بدتری نسبت به انتخاب از یک جدول دارد.
  • با TPT به شما این امکان را می‌دهد که ستون‌های زیر کلاس مورد نیاز را ایجاد کنید
    با TPT، هر زیر کلاس جدول مخصوص به خود را دارد، بنابراین می توانید ستون های مورد نیاز را ایجاد کنید (با افزودن ویژگی [Required]). به عبارت دیگر، می توانید آنها را NOT NULL کنید.

    از طرف دیگر با TPH، تمام ستون های زیر کلاس در یک جدول هستند. این بدان معنی است که آنها باید nullable شوند. وقتی رکوردی را برای یک زیر کلاس (مثلاً CLIENT) وارد می کنید، برای ستون های متعلق به زیر کلاس دیگر (مثلاً USER) مقداری نخواهد داشت. بنابراین منطقی است که نمی توان این ستون ها required باشد. حتی اگر ویژگی [required] را اضافه کنید، هنگام ایجاد MIGRATION نادیده گرفته می‌شود و ستون روی nullable تنظیم می‌شود. اگر ستون را مجبور کنید NOT NULL باشد، هنگام درج رکوردها با مشکل مواجه خواهید شد، بنابراین از انجام این کار خودداری کنید.


بیشتر بخوانید : دوره آموزشی Entity FrameWork Core - قسمت چهاردهم

بیشتر بخوانید : دوره آموزشی Entity FrameWork Core

بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core

https://zarinp.al/farshidazizi

دوره آموزشیentity framework coreef coreaspnetcore
Software Engineer
شاید از این پست‌ها خوشتان بیاید