دوره آموزشی Entity FrameWork Core - قسمت نهم
یک رابطه(Relationship) نحوه ارتباط دو موجودیت را با یکدیگر تعریف می کند. در یک پایگاه داده رابطه ای، این با یک محدودیت کلید خارجی(foreign key constraint) نشان داده می شود.
یادآوری : Constraint های sql شامل موارد زیر میشوند :
* NOT NULL(مشخص میکند که فیلد، مقادیر null را نمی پذیرد .)
* UNIQUE(مقدار فیلد می بایست در تمامی رکورد های یک جدول منحصر به فرد باشد.)
* PRIMARY KEY(کلید اصلی یک constraint است هیچ دو رکوردی در جدول دارای مقدار یکسان در کلید اصلی نیستند .)
* FOREIGN KEY(رابطه بین جداول را مشخص می کند هنگامی که رابطه والد و فرزندی بین رکورد های دو جدول برقرار است foreign key constraint اجاره نمیدهد رکورد والدی حذف شود که دارای فرزند است و یا اینکه رکورد فرزندی درج شود بدون اینکه فاقد کلید اصلی والد خود باشد.)
* CHECK(جهت محدود کردن مقادیر ورودی در فیلد.)
* DEFAULT(تعیین مقدار پیشفرض برای فیلد. اگر مقداری در فیلد وارد نشود این مقدار بصورت خودکار در فیلد درج می شود)
تعدادی اصطلاح برای توصیف روابط وجود دارد:
قطعه کد زیر رابطه یک به چند بین blog و post را نشان می دهد :
public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } public Blog Blog { get; set; } }
به طور پیش فرض، زمانی که یک پراپرتی ناوبری در یک entity type کشف شود، یک رابطه ایجاد می شود. یک پراپرتی در صورتی یک پراپرتی ناوبری در نظر گرفته می شود که نوع مورد نظر به عنوان یک scalar type توسط ارائه دهنده پایگاه داده فعلی نگاشت نشود.
اما scalar type چیست ؟
پراپرتی های نوع اولیه(primitive type properties) را ویژگی های اسکالر می نامند. هر ویژگی اسکالر به ستونی در جدول پایگاه داده نگاشت می شود که یک داده واقعی را ذخیره می کند. به عنوان مثال StudentID، StudentName، DateOfBirth، Photo، Height، Weight ویژگی های اسکالر در کلاس موجودیت Student هستند.
روابطی که طبق قرارداد کشف می شوند همیشه کلید اصلی موجودیت اصلی را هدف قرار می دهند. برای هدف قرار دادن یک کلید جایگزین(alternate key)، پیکربندی اضافی باید با استفاده از Fluent API انجام شود.
متداول ترین الگوی روابط این است که ویژگی های ناوبری(navigation properties) در هر دو انتهای رابطه و یک ویژگی کلید خارجی(foreign key) در کلاس موجودیت وابسته(dependent entity) تعریف شده باشد.
<navigation property name><principal key property name>
<navigation property name>Id
<principal entity name><principal key property name>
<principal entity name>Id
public class Blog //Principal entity { public int BlogId { get; set; } //principal key public string Url { get; set; } public List<Post> Posts { get; set; } //Collection navigation property } public class Post //Dependent entity { public int PostId { get; set; } //principal key public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } //Foreign key public Blog Blog { get; set; } //Inverse navigation property And Reference navigation property }
اگر Post.BlogId از نوعی باشد که با کلید اصلی سازگار نباشد، به عنوان کلید خارجی پیکربندی نمیشود.(type ها می بایست یکسان باشد.)
گنجاندن تنها یک ویژگی ناوبری (بدون ناوبری معکوس و بدون ویژگی کلید خارجی) برای داشتن یک رابطه تعریف شده توسط قرارداد کافی است. شما همچنین می توانید یک ویژگی ناوبری واحد و یک ویژگی کلید خارجی داشته باشید.
public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } }
محدودیت ها (Limitations) :
هنگامی که چندین ویژگی ناوبری بین دو entity type تعریف شده است (یعنی بیش از یک جفت navigations که به یکدیگر اشاره می کنند) روابط نشان داده شده توسط ویژگی های ناوبری مبهم هستند. برای رفع ابهام باید آنها را به صورت دستی پیکربندی کنید.
حذف آبشاری (Cascade delete) :
طبق قرارداد، cascade delete برای روابط مورد نیاز( Required Relationships) روی Cascade و برای روابط اختیاری(Optional Relationships) برروی ClientSetNull تنظیم می شود.
cascade یعنی موجودیت های وابسته نیز حذف می شوند.
ClientSetNull به این معنی است که موجودیت های وابسته که در حافظه بارگذاری نمی شوند، بدون تغییر باقی می مانند و باید به صورت دستی حذف شوند، یا به روز شوند تا به یک موجودیت اصلی معتبر اشاره کنند. برای موجودیت هایی که در حافظه بارگذاری می شوند، EF Core سعی می کند خصوصیات کلید خارجی را null کند.
روابط اجباری و اختیاری(Required and optional relationships)
میتوانید از Fluent API برای پیکربندی اینکه آیا رابطه الزامی است یا اختیاری استفاده کنید. در نهایت این کنترل می کند که آیا ویژگی کلید خارجی الزامی یا اختیاری است. این زمانی که از کلید خارجی حالت سایه
( shadow frogin key ) استفاده می کنید بسیار مفید است. اگر دارای یک ویژگی کلید خارجی در کلاس موجودیت خود هستید، الزامی بودن رابطه بر اساس الزامی یا اختیاری بودن پراپرتی کلید خارجی تعیین میشود.(یک پراپرتی در صورتی اختیاری در نظر گرفته می شود که معتبر باشد که حاوی null باشد. اگر null یک مقدار معتبر برای تخصیص به یک ویژگی نباشد، به عنوان یک ویژگی الزامی در نظر گرفته می شود. هنگام نگاشت به یک طرح پایگاه داده رابطهای، ویژگیهای مورد نیاز بهعنوان ستونهای غیر قابل تهی و ویژگیهای اختیاری بهعنوان ستونهای nullable ایجاد میشوند.)
ویژگی های کلید خارجی بر روی نوع موجودیت وابسته قرار دارند، بنابراین اگر به صورت الزامی پیکربندی شوند به این معنی است که هر موجودیت وابسته باید یک موجودیت اصلی متناظر داشته باشد.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasOne(p => p.Blog)
.WithMany(b => b.Posts)
.IsRequired();
}
فراخوانی IsRequired(false) نیز ویژگی کلید خارجی را اختیاری می کند مگر اینکه به شکل دیگری پیکربندی شده باشد.
حذف آبشاري (Cascade delete) :
میتوانید از Fluent API برای پیکربندی رفتار حذف آبشاری برای یک رابطه مشخص استفاده کنید.
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts) .OnDelete(DeleteBehavior.Cascade); }
پیکربندی دستی (Manual configuration) :
internal class MyContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts); } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }
public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int AuthorUserId { get; set; } public User Author { get; set; } public int ContributorUserId { get; set; } public User Contributor { get; set; } } public class User { public string UserId { get; set; } public string FirstName { get; set; } public string LastName { get; set; } [InverseProperty("Author")] public List<Post> AuthoredPosts { get; set; } [InverseProperty("Contributor")] public List<Post> ContributedToPosts { get; set; } }
شما فقط میتوانید از [Required] در ویژگیهای موجودیت وابسته استفاده کنید تا بر ضرورت رابطه تأثیر بگذارد. [Required] در مسیریابی موجودیت اصلی معمولاً نادیده گرفته میشود، اما ممکن است باعث شود که موجودیت به واحد وابسته تبدیل شود.
The data annotations[ForeignKey]
and[InverseProperty]
are available in theSystem.ComponentModel.DataAnnotations.Schema
namespace.
[Required]
is available in theSystem.ComponentModel.DataAnnotations
namespace.
اگر فقط یک ویژگی ناوبری دارید، پس overloadهای بدون پارامتر WithOne و WithMany برای آن وجود دارد. این نشان می دهد که از نظر مفهومی یک reference یا collectionی در یک سوی رابطه وجود دارد، اما هیچ ویژگی ناوبری دیگری در کلاس مرتبط وجود ندارد.
internal class MyContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .HasMany(b => b.Posts) .WithOne(); modelBuilder.Entity<Blog>() .Navigation(b => b.Posts) .UsePropertyAccessMode(PropertyAccessMode.Property); } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } }
توجه داشته باشید پیکربندی فوق برای ایجاد یک پراپرتی ناوبری قابل استفاده نیست. این فقط برای پیکربندی یک navigation property که قبلاً با تعریف یک رابطه یا از یک قرارداد ایجاد شده است استفاده می شود.
پیکربندی کلید خارجی (Foreign key) :
internal class MyContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts) .HasForeignKey(p => p.BlogForeignKey); } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogForeignKey { get; set; } public Blog Blog { get; set; } }
internal class MyContext : DbContext { public DbSet<Car> Cars { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Car>() .HasKey(c => new { c.State, c.LicensePlate }); modelBuilder.Entity<RecordOfSale>() .HasOne(s => s.Car) .WithMany(c => c.SaleHistory) .HasForeignKey(s => new { s.CarState, s.CarLicensePlate }); } } public class Car { public string State { get; set; } public string LicensePlate { get; set; } public string Make { get; set; } public string Model { get; set; } public List<RecordOfSale> SaleHistory { get; set; } } public class RecordOfSale { public int RecordOfSaleId { get; set; } public DateTime DateSold { get; set; } public decimal Price { get; set; } public string CarState { get; set; } public string CarLicensePlate { get; set; } public Car Car { get; set; } }
public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } [ForeignKey("BlogForeignKey")] public int BlogForeignKey { get; set; } public Blog Blog { get; set; } }
کلید خارجی سایه (Shadow foreign key) :
می توانید از overload متد HasForeignKey(...) برای پیکربندی یک ویژگی سایه به عنوان یک کلید خارجی استفاده کنید. توصیه می کنیم قبل از استفاده از آن به عنوان کلید خارجی، ویژگی shadow را به صراحت به مدل اضافه کنید.
internal class MyContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Add the shadow property to the model modelBuilder.Entity<Post>() .Property<int>("BlogForeignKey"); // Use the shadow property as a foreign key modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts) .HasForeignKey("BlogForeignKey"); } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } public List<Post> Posts { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public Blog Blog { get; set; } }
لزوماً نیازی به ارائه پراپرتی ناوبری ندارید. شما به سادگی می توانید یک کلید خارجی در یک طرف رابطه ارائه دهید.
internal class MyContext : DbContext { public DbSet<Blog> Blogs { get; set; } public DbSet<Post> Posts { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .HasOne<Blog>() .WithMany() .HasForeignKey(p => p.BlogId); } } public class Blog { public int BlogId { get; set; } public string Url { get; set; } } public class Post { public int PostId { get; set; } public string Title { get; set; } public string Content { get; set; } public int BlogId { get; set; } }
کلید اصلی (Principal key) :
اگر میخواهید کلید خارجی به ویژگی دیگری غیر از کلید اصلی اشاره کند، میتوانید از Fluent API برای پیکربندی ویژگی کلید اصلی برای رابطه استفاده کنید. ویژگی ای که به عنوان کلید اصلی پیکربندی می کنید به طور خودکار به عنوان یک کلید جایگزین تنظیم می شود.
internal class MyContext : DbContext { public DbSet<Car> Cars { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<RecordOfSale>() .HasOne(s => s.Car) .WithMany(c => c.SaleHistory) .HasForeignKey(s => s.CarLicensePlate) .HasPrincipalKey(c => c.LicensePlate) } } public class Car { public int CarId { get; set; } public string LicensePlate { get; set; } public string Make { get; set; } public string Model { get; set; } public List<RecordOfSale> SaleHistory { get; set; } } public class RecordOfSale { public int RecordOfSaleId { get; set; } public DateTime DateSold { get; set; } public decimal Price { get; set; } public string CarLicensePlate { get; set; } public Car Car { get; set; } }
internal class MyContext : DbContext { public DbSet<Car> Cars { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<RecordOfSale>() .HasOne(s => s.Car) .WithMany(c => c.SaleHistory) .HasForeignKey(s => new { s.CarState, s.CarLicensePlate }) .HasPrincipalKey(c => new { c.State, c.LicensePlate }); } } public class Car { public int CarId { get; set; } public string State { get; set; } public string LicensePlate { get; set; } public string Make { get; set; } public string Model { get; set; } public List<RecordOfSale> SaleHistory { get; set; } } public class RecordOfSale { public int RecordOfSaleId { get; set; } public DateTime DateSold { get; set; } public decimal Price { get; set; } public string CarState { get; set; } public string CarLicensePlate { get; set; } public Car Car { get; set; } }
توجه داشته باشید ترتیبی که ویژگی های کلید اصلی را مشخص می کنید باید با ترتیبی که برای کلید خارجی مشخص شده اند مطابقت داشته باشد.
بیشتر بخوانید : دوره آموزشی Entity FrameWork Core - قسمت يازدهم
بیشتر بخوانید : دوره آموزشی Entity FrameWork Core
بیشتر بخوانید : نقشه راه توسعه دهندگان Asp.NET Core