تنظیمات روابط یک به چند در EF Core
همان اسکریپت ابتدای مطلب «شروع به کار با EF Core 1.0 - قسمت 4 - کار با بانکهای اطلاعاتی از پیش موجود» را درنظر بگیرید. رابطهی تعریف شدهی در آن از نوع one-to-many است: یک بلاگ که میتواند چندین مطلب را داشته باشد.
اگر EF Core را وادار به تولید نگاشتهای Code First معادل آن کنیم، به این خروجیها خواهیم رسید:
الف) با استفاده از روش Fluent API
دستور استفاده شده برای مهندسی معکوس بانک اطلاعاتی نمونه:
dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose
using System; using System.Collections.Generic; namespace Core1RtmEmptyTest.Entities { public partial class Blog { public Blog() { Post = new HashSet<Post>(); } public int BlogId { get; set; } public string Url { get; set; } public virtual ICollection<Post> Post { get; set; } } }
using System; using System.Collections.Generic; namespace Core1RtmEmptyTest.Entities { public partial class Post { public int PostId { get; set; } public string Content { get; set; } public string Title { get; set; } public virtual Blog Blog { get; set; } public int BlogId { get; set; } } }
using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; namespace Core1RtmEmptyTest.Entities { public partial class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>(entity => { entity.Property(e => e.Url).IsRequired(); }); modelBuilder.Entity<Post>(entity => { entity.HasOne(d => d.Blog) .WithMany(p => p.Post) .HasForeignKey(d => d.BlogId); }); } public virtual DbSet<Blog> Blog { get; set; } public virtual DbSet<Post> Post { get; set; } } }
نحوهی تشخیص خودکار روابط
EF Core به صورت پیش فرض، روابط را بر اساس ارجاعات بین کلاسها تشخیص میدهد. در اینجا به خاصیت Blog نام navigation property را میدهند:
public virtual Blog Blog { get; set; }
public virtual ICollection<Post> Post { get; set; }
نحوهی تشخیص خودکار کلیدهای خارجی
اگر در یک طرف رابطهی تشخیص داده شده، خاصیتی با یکی از سه نام زیر وجود داشت:
<primary key property name> <navigation property name><primary key property name> <principal entity name><primary key property name>
برای مثال در رابطهی فوق، نام خاصیت BlogId دقیقا بر اساس همان الگوی <primary key property name> طرف دیگر رابطهاست:
public virtual Blog Blog { get; set; } public int BlogId { get; set; }
تا اینجا اگر مطلب را دنبال کرده باشید به این نتیجه خواهید رسید که دو کلاس فوق، اساسا نیازی به هیچ نوع تنظیم Fluent و یا Data annotations ایی برای برقراری ارتباط یک به چند ندارند. چون روابط بین آنها بر اساس خواص راهبری (navigation property) و همچنین الگوی <primary key property name>، به صورت خودکار قابل تشخیص و تنظیم است. به علاوه ... در هر طرف رابطه، فقط یک navigation property وجود دارد و نیازی به تنظیم دستی سر دیگر رابطه نیست.
استفاده از Fluent API برای تنظیم رابطهی One-to-Many
در تنظیمات فوق، در متد OnModelCreating، ذکر صریح این روابط را صرفا جهت از بین بردن هرگونه ابهامی مشاهده میکنید:
modelBuilder.Entity<Post>(entity => { entity.HasOne(d => d.Blog) .WithMany(p => p.Post) .HasForeignKey(d => d.BlogId); });
مرحلهی بعد، مشخص کردن سر دیگر رابطه (inverse navigation) است. اینکار توسط یکی از متدهای WithOne و یا WithMany انجام میشود.
متدهایی که اسامی فرد دارند مانند HasOne/WithOne به یک navigation property ساده اشاره میکنند.
متدهایی که اسامی جمع دارند مانند HasMany/WithMany به collection navigation properties اشاره خواهند کرد.
متد HasForeignKey نیز برای ذکر صریح کلید خارجی بکار رفتهاست.
ب) با استفاده از روش data-annotations
دستور استفاده شده برای مهندسی معکوس بانک اطلاعاتی نمونه:
dotnet ef dbcontext scaffold "Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true" Microsoft.EntityFrameworkCore.SqlServer -o Entities --context MyDBDataContext --verbose -a
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Core1RtmEmptyTest.Entities { public partial class Blog { public Blog() { Post = new HashSet<Post>(); } public int BlogId { get; set; } [Required] public string Url { get; set; } [InverseProperty("Blog")] public virtual ICollection<Post> Post { get; set; } } }
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Core1RtmEmptyTest.Entities { public partial class Post { public int PostId { get; set; } public string Content { get; set; } public string Title { get; set; } [ForeignKey("BlogId")] [InverseProperty("Post")] public virtual Blog Blog { get; set; } public int BlogId { get; set; } } }
using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata; namespace Core1RtmEmptyTest.Entities { public partial class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=BloggingCore2016;Integrated Security = true"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { } public virtual DbSet<Blog> Blog { get; set; } public virtual DbSet<Post> Post { get; set; } } }
[ForeignKey("BlogId")] [InverseProperty("Post")] public virtual Blog Blog { get; set; } public int BlogId { get; set; }
در این حالت (داشتن بیش از یک خاصیت راهبری)، باید ویژگی InverseProperty را نیز به سر دوم رابطه، اعمال کرد.
[InverseProperty("Blog")] public virtual ICollection<Post> Post { get; set; }
مطالب تکمیلی
علت virtual بودن خواص راهبری تولید شده
اگر دقت کنید، EF Core کدی را که تولید کردهاست، به همراه خاصیتهایی virtual است:
public virtual Blog Blog { get; set; }
الف) پیاده سازی lazy loading (بارگذاری خودکار اعضای مرتبط (همان خواص راهبری) با اولین دسترسی به آنها)
ب) پیاده سازی change tracking
مبحث lazy loading فعلا در EF Core 1.0 پشتیبانی نمیشود. اما change tracking آن فعال است.
بنابراین اگر مشاهده کردید خواص راهبری به صورت virtual تعریف شدهاند، علت آن فعال سازی lazy loading است و اگر سایر خواص به صورت virtual تعریف شدهاند، هدف اصلی آن بهبود عملکرد سیستم change tracking است.
همچنین اگر دقت کرده باشید، نوع مجموعهها نیز ICollection ذکر شدهاست. این مورد نیز یکی دیگر از پیش فرضهای توکار EF Core است؛ در جهت تشکیل پروکسیها بر روی خواص راهبری مجموعهای (علاوه بر virtual تعریف کردن آنها). عنوان شدهاست که اگر برای مثال از List استفاده کنید (پیاده سازی اینترفیس) یا هر اینترفیس دیگری که از ICollection مشتق شدهاست، این پروکسیها تشکیل نخواهند شد.
واکشی اعضای به هم مرتبط
همانطور که عنوان شد، نگارش اول EF Core برخلاف EF 6.x از Lazy loading پشتیبانی نمیکند. البته این مساله در کل مورد مثبتی است؛ خصوصا در برنامههای وب! چون استفادهی نادرست از Lazy loading که به select n+1 نیز مشهور است، سبب رفت و برگشتهای بیشماری به بانک اطلاعاتی میشود و عموم برنامه نویسهای وب باید مدام توسط برنامههای Profiler بررسی کنند که آیا این مساله رخ دادهاست یا خیر. فعلا EF Core از این مشکل در امان است!
اما ... اگر به روش کار EF 6.x عادت کرده باشید، قطعه کد ذیل:
var firstPost = context.Post.First(); Console.WriteLine(firstPost.Blog.Url);
System.NullReferenceException Object reference not set to an instance of an object.
برای رفع این مشکل باید توسط متد Include، سبب لغو عملیات Lazy loading و واکشی صریح Blog مرتبط شویم که اصطلاحا به آن eager loading میگویند:
var firstPost = context.Post.Include(x => x.Blog).First(); Console.WriteLine(firstPost.Blog.Url);
نکتهای در مورد سطوح بارگذاری اعضای به هم مرتبط در EF Core
متد Include ایی را که تا اینجا مشاهده کردید، با EF 6.x تفاوتی ندارد. برای مثال اگر شیء Blog حاوی خواص راهبری Posts و همچنین Owner باشد، برای بارگذاری این اعضای مرتبط، میتوان همانند قبل، متدهای Include را پشت سر هم ذکر کرد:
var blogs = context.Blogs .Include(blog => blog.Posts) .Include(blog => blog.Owner) .ToList();
var blogs = context.Blogs .Include(blog => blog.Posts) .ThenInclude(post => post.Author) .ToList();
همچنین در اینجا امکان ذکر زنجیروار متدهای ThenInclude هم هست:
var blogs = context.Blogs .Include(blog => blog.Posts) .ThenInclude(post => post.Author) .ThenInclude(author => author.Photo) .ToList();
به علاوه امکان ذکر چندین ریشه و چندین زیر ریشه هم وجود دارند:
var blogs = context.Blogs .Include(blog => blog.Posts) .ThenInclude(post => post.Author) .ThenInclude(author => author.Photo) .Include(blog => blog.Owner) .ThenInclude(owner => owner.Photo) .ToList();
یک نکته: متد Include تنها زمانی درنظر گرفته خواهد شد که نوع خروجی نهایی کوئری، دقیقا از نوع موجودیتی باشد که با آن شروع به کار کردهایم. برای مثال اگر در این بین یک Select اضافه شود و فقط تنها تعدادی از خواص Blog واکشی شوند، از تمام Includeهای ذکر شده صرفنظر میشود؛ مانند کوئری ذیل:
var blogs = context.Blogs .Include(blog => blog.Posts) .Select(blog => new { Id = blog.BlogId, Url = blog.Url }) .ToList();
تنظیمات حذف آبشاری در رابطهی one-to-many
زمانیکه در رابطهی one-to-many قسمت principal (والد رابطه) و یا همان Blog در مثال جاری حذف میشود، سه اتفاق برای فرزندان آن میسر خواهند بود:
الف) Cascade : در این حالت ردیفهای فرزندان وابسته نیز حذف خواهند شد.
باید دقت داشت که حالت Cascade فقط برای موجودیتهایی اعمال میشود که توسط Context بارگذاری شده و در آن وجود دارند. اگر میخواهید سایر موجودیتهای مرتبط نیز با این روش حذف شوند، باید در سمت دیتابیس نیز تنظیماتی مانند ON DELETE CASCADE زیر نیز وجود داشته باشند:
CONSTRAINT [FK_Post_Blog_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blog] ([BlogId]) ON DELETE CASCADE
modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts) .OnDelete(DeleteBehavior.Cascade);
ج) Restrict: هیچ تغییری بر روی فرزندان رابطه رخ نمیدهد.
یک نکته: به صورت پیش فرض اگر رابطهی one-to-many، به Required تنظیم شود، حالت حذف آن cascade خواهد بود. در غیراینصورت برای حالتهای Optional، حالت SetNull تنظیم میگردد:
modelBuilder.Entity<Post>() .HasOne(p => p.Blog) .WithMany(b => b.Posts) .IsRequired();
به علاوه باید دقت داشت، همان مباحث «تعیین اجباری بودن یا نبودن ستونها در EF Core» در قسمت قبل، در اینجا هم صادق است. برای مثال چون BlogId (کلید خارجی در کلاس Post) از نوع int است و نال پذیر نیست، بنابراین از دیدگاه EF Core یک فیلد اجباری درنظر گرفته میشود. به همین جهت است که در کدهای تولید شدهی توسط EF Core در ابتدای بحث، ذکر متد IsRequired و یا OnDelete را مشاهده نمیکنید.
بنابراین اگر میخواهید حالت SetNull را فعال کنید، باید این کلید خارجی را نیز نال پذیر و به صورت int? BlogId ذکر کنید تا optional درنظر گرفته شود.