پیشنیازها
- بررسی نحوه تعریف نگاشت جداول خود ارجاع دهنده (Self Referencing Entity)
- مباحث تکمیلی مدلهای خود ارجاع دهنده در EF Code first
- آشنایی با SQL Server Common Table Expressions - CTE
- بدست آوردن برگهای یک درخت توسط Recursive CTE
در پیشنیازهای بحث، روش تعریف روابط خود ارجاع دهنده و یا Self Referencing را تا EF 6.x میتوانید مطالعه کنید. در این قسمت قصد داریم معادل این روشها را در EF Core بررسی کنیم.
روش تعریف روابط خود ارجاع دهنده توسط Fluent API در EF Core
اگر همان مدل کامنتهای چندسطحی یک بلاگ را درنظر بگیریم:
اینبار تنظیمات Fluent API معادل EF Core آن به صورت ذیل خواهد بود:
هدف از مدلهای خود ارجاع دهنده، مدلسازی اطلاعات چند سطحی است؛ مانند منوهای چندسطحی یک سایت، کامنتهای چند سطحی یک مطلب، سلسله مراتب کارمندان یک شرکت و امثال آن.
نکتههای مهم مدلسازی این نوع روابط، موارد ذیل هستند:
الف) وجود خاصیتی از جنس کلاس اصلی در همان کلاس
ب) که در حقیقت مشخص میکند، والد رکورد جاری کدام رکورد قبلی است:
برای اینکه چنین رابطهای تشکیل شود، باید این فیلد، مقدارش را از کلید اصلی جدول تامین کند (تشکیل یک کلید خارجی که به کلید اصلی جدول اشاره میکند).
علت نال پذیر بودن این خاصیت، نیاز به ثبت ریشهای است که خودش والد است و فرزند رکوردی پیشین، نیست.
ج) همچنین برای سهولت دریافت فرزندان یک ریشه، مجموعهای از جنس همان کلاس نیز تشکیل میشود.
روش تعریف روابط خود ارجاع دهنده توسط Data Annotations در EF Core
در ادامه معادل تنظیمات فوق را توسط ویژگیها ملاحظه میکنید که در اینجا توسط ویژگیهای InverseProperty و ForeignKey، کار تشکیل روابط صورت گرفتهاست:
البته قسمت تشکیل ایندکس بر روی ReplyId را فقط توسط Fluent API میتوان انجام داد:
ثبت اطلاعات و کوئری گرفتن از روابط خود ارجاع دهنده در EF Core
در اینجا نحوهی ثبت دو سری نظر و زیر نظر را مشاهده میکنید:
نظرات اصلی، با ReplyId مساوی نال قابل تشخیص هستند. سایر نظرات، توسط همین ReplyId به یکی از Idهای موجود، متصل شدهاند.
در تصویر فوق و با توجه به اطلاعات ثبت شده، فرض کنید میخواهیم ریشهی با id مساوی 1 و تمام زیر ریشههای آنرا بیابیم. انجام یک چنین کاری نه در EF Core و نه در EF 6.x، پشتیبانی نمیشود. بدیهی است در اینجا هنوز روشهای Include و ThenInclue هم جواب میدهند؛ اما چون Lazy loading فعال نیست، عملا نمیتوان تمام زیر ریشهها را یافت و همچنین به چندین و چند رفت و برگشت به ازای هر زیر ریشه خواهیم رسید که اصلا بهینه نیست.
برای اینکار نیاز است مستقیما کوئری نویسی کرد که در مطلب «شروع به کار با EF Core 1.0 - قسمت 10 - استفاده از امکانات بومی بانکهای اطلاعاتی» زیر ساخت آنرا بررسی کردیم:
زمانیکه کدهای فوق اجرا میشوند، تنها دو کوئری ذیل به بانک اطلاعاتی ارسال خواهند شد:
که لیست Idهای تمام زیر ریشههای مربوط به id مساوی یک را بر میگرداند:
و پس از آن:
اکنون که Idهای مساوی 3 و 5 را یافتیم، با استفاده از متد Contains آنها را تبدیل به where in کرده و لیست نهایی گزارش را تهیه میکنیم:
یافتن Idهای زیر ریشهها توسط روش CTE (پیشنیازهای ابتدای بحث) و سپس کوئری گرفتن بر روی این Idها، بهینهترین روشیاست که در EF میتوان جهت کار با مدلهای خود ارجاع دهنده بکار گرفت.
- بررسی نحوه تعریف نگاشت جداول خود ارجاع دهنده (Self Referencing Entity)
- مباحث تکمیلی مدلهای خود ارجاع دهنده در EF Code first
- آشنایی با SQL Server Common Table Expressions - CTE
- بدست آوردن برگهای یک درخت توسط Recursive CTE
در پیشنیازهای بحث، روش تعریف روابط خود ارجاع دهنده و یا Self Referencing را تا EF 6.x میتوانید مطالعه کنید. در این قسمت قصد داریم معادل این روشها را در EF Core بررسی کنیم.
روش تعریف روابط خود ارجاع دهنده توسط Fluent API در EF Core
اگر همان مدل کامنتهای چندسطحی یک بلاگ را درنظر بگیریم:
public class BlogComment { public int Id { get; set; } public string Body { get; set; } public DateTime Date1 { get; set; } public virtual BlogComment Reply { get; set; } public int? ReplyId { get; set; } public virtual ICollection<BlogComment> Children { get; set; } }
public class MyDBDataContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"Data Source=(local);Initial Catalog=testdb2;Integrated Security = true"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<BlogComment>(entity => { entity.HasIndex(e => e.ReplyId); entity.HasOne(d => d.Reply) .WithMany(p => p.Children) .HasForeignKey(d => d.ReplyId); }); } public virtual DbSet<BlogComment> BlogComments { get; set; } }
نکتههای مهم مدلسازی این نوع روابط، موارد ذیل هستند:
الف) وجود خاصیتی از جنس کلاس اصلی در همان کلاس
public virtual BlogComment Reply { get; set; }
public int? ReplyId { get; set; }
علت نال پذیر بودن این خاصیت، نیاز به ثبت ریشهای است که خودش والد است و فرزند رکوردی پیشین، نیست.
ج) همچنین برای سهولت دریافت فرزندان یک ریشه، مجموعهای از جنس همان کلاس نیز تشکیل میشود.
public virtual ICollection<BlogComment> Children { get; set; }
روش تعریف روابط خود ارجاع دهنده توسط Data Annotations در EF Core
در ادامه معادل تنظیمات فوق را توسط ویژگیها ملاحظه میکنید که در اینجا توسط ویژگیهای InverseProperty و ForeignKey، کار تشکیل روابط صورت گرفتهاست:
public class BlogComment { public int Id { get; set; } public string Body { get; set; } public DateTime Date1 { get; set; } [ForeignKey("ReplyId")] [InverseProperty("Children")] public virtual BlogComment Reply { get; set; } public int? ReplyId { get; set; } [InverseProperty("Reply")] public virtual ICollection<BlogComment> Children { get; set; } }
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<BlogComment>(entity => { entity.HasIndex(e => e.ReplyId); }); }
ثبت اطلاعات و کوئری گرفتن از روابط خود ارجاع دهنده در EF Core
در اینجا نحوهی ثبت دو سری نظر و زیر نظر را مشاهده میکنید:
var comment1 = new BlogComment { Body = "نظر من این است که" }; var comment12 = new BlogComment { Body = "پاسخی به نظر اول", Reply = comment1 }; var comment121 = new BlogComment { Body = "پاسخی به پاسخ به نظر اول", Reply = comment12 }; context.BlogComments.Add(comment121); var comment2 = new BlogComment { Body = "نظر من این بود که" }; var comment22 = new BlogComment { Body = "پاسخی به نظر قبلی", Reply = comment2 }; var comment221 = new BlogComment { Body = "پاسخی به پاسخ به نظر من اول", Reply = comment22 }; context.BlogComments.Add(comment221); context.SaveChanges();
نظرات اصلی، با ReplyId مساوی نال قابل تشخیص هستند. سایر نظرات، توسط همین ReplyId به یکی از Idهای موجود، متصل شدهاند.
در تصویر فوق و با توجه به اطلاعات ثبت شده، فرض کنید میخواهیم ریشهی با id مساوی 1 و تمام زیر ریشههای آنرا بیابیم. انجام یک چنین کاری نه در EF Core و نه در EF 6.x، پشتیبانی نمیشود. بدیهی است در اینجا هنوز روشهای Include و ThenInclue هم جواب میدهند؛ اما چون Lazy loading فعال نیست، عملا نمیتوان تمام زیر ریشهها را یافت و همچنین به چندین و چند رفت و برگشت به ازای هر زیر ریشه خواهیم رسید که اصلا بهینه نیست.
برای اینکار نیاز است مستقیما کوئری نویسی کرد که در مطلب «شروع به کار با EF Core 1.0 - قسمت 10 - استفاده از امکانات بومی بانکهای اطلاعاتی» زیر ساخت آنرا بررسی کردیم:
var id = 1; var childIds = new List<int>(); var connection = context.Database.GetDbConnection(); connection.Open(); var command = connection.CreateCommand(); command.CommandText = @"WITH Hierachy(ChildId, ParentId) AS ( SELECT Id, ReplyId FROM BlogComments UNION ALL SELECT h.ChildId, bc.ReplyId FROM BlogComments bc INNER JOIN Hierachy h ON bc.Id = h.ParentId ) SELECT h.ChildId FROM Hierachy h WHERE h.ParentId = @Id"; command.Parameters.Add(new SqlParameter("id", id)); var reader = command.ExecuteReader(); while (reader.Read()) { var childId = (int)reader[0]; childIds.Add(childId); } reader.Dispose(); var list = context.BlogComments .Where(blogComment => blogComment.Id == id || childIds.Contains(blogComment.Id)) .ToList();
exec sp_executesql N'WITH Hierachy(ChildId, ParentId) AS ( SELECT Id, ReplyId FROM BlogComments UNION ALL SELECT h.ChildId, bc.ReplyId FROM BlogComments bc INNER JOIN Hierachy h ON bc.Id = h.ParentId ) SELECT h.ChildId FROM Hierachy h WHERE h.ParentId = @Id',N'@id int',@id=1
و پس از آن:
exec sp_executesql N'SELECT [blogComment].[Id], [blogComment].[Body], [blogComment].[Date1], [blogComment].[ReplyId] FROM [BlogComments] AS [blogComment] WHERE [blogComment].[Id] IN (@__id_0, 3, 5)',N'@__id_0 int',@__id_0=1
یافتن Idهای زیر ریشهها توسط روش CTE (پیشنیازهای ابتدای بحث) و سپس کوئری گرفتن بر روی این Idها، بهینهترین روشیاست که در EF میتوان جهت کار با مدلهای خود ارجاع دهنده بکار گرفت.