در مطلب قبل نحوهی ایجاد روابط Polymorphic را بررسی کردیم و همچنین چندین راهحل جایگزین را نیز ارائه دادیم. همانطور که عنوان شد این نوع روابط اساساً از لحاظ طراحی دیتابیس اصولی نیستند و تا حد امکان نباید استفاده شوند. این نوع روابط بیشتر ORM friendly هستند و اکثر فریمورکهای غیرداتنتی به عنوان یک گزینهی توکار، امکان ایجاد این روابط را فراهم میکنند. به عنوان مثال درLaravel Eloquent ORM به صورت توکار از این قابلیت پشتیبانی میشود:
<?php namespace App; use Illuminate\Database\Eloquent\Model; class Comment extends Model { /** * Get the owning commentable model. */ public function commentable() { return $this->morphTo(); } } class Post extends Model { /** * Get all of the post's comments. */ public function comments() { return $this->morphMany('App\Comment', 'commentable'); } } class Video extends Model { /** * Get all of the video's comments. */ public function comments() { return $this->morphMany('App\Comment', 'commentable'); } }
اما در Entity Framework Core هنوز این قابلیت پیادهسازی نشده است. در اینجا میتوانید وضعیت پیادهسازی Polymorphic Association را پیگیری کنید. پیادهسازی این قابلیت از این جهت مهم است که امکان کوئری نویسی را برای این نوع روابط سادهتر خواهد کرد به عنوان مثال در کدهای PHP فوق جهت واکشی کامنتهای یک مطلب میتوانیم اینگونه عمل کنیم:
$post = App\Post::find(1); foreach ($post->comments as $comment) { // }
همچنین امکان واکشی owner این رابطه را نیز حین کار با کامنتها را خواهیم داشت:
$comment = App\Comment::find(1); $commentable = $comment->commentable;
در ادامه میخواهیم معادل LINQ آن را پیادهسازی کنیم. در مطلب قبل مدل Comment این چنین ساختاری داشت:
public enum CommentType { Article, Video, Event } public class Comment { public int Id { get; set; } public string CommentText { get; set; } public string User { get; set; } public int? TypeId { get; set; } public CommentType CommentType { get; set; } }
در اینجا همانطور که مشاهده میکنید هیچگونه ارتباط معناداری بین Comment و همچنین owner رابطه (که ممکن است هر کدام از مقادیر Enum فوق باشد) وجود ندارد. اگر این مدل به تنها یک مدل مثلاً Article اشاره داشته باشد، نیاز به تعیین Navigation Property در دو طرف رابطه خواهد بود:
public class Comment { public int Id { get; set; } public string CommentText { get; set; } public string User { get; set; } public virtual Article Article { get; set; } public int ArticleId { get; set; } } public class Article { public int Id { get; set; } public string Title { get; set; } public string Slug { get; set; } public string Description { get; set; } public virtual ICollection<Article> Articles { get; set; } }
اما از آنجائیکه رابطه یک حالت پویا دارد، نمیتوانیم به صورت صریح نوع ارجاعات را در دو طرف رابطه تعیین کنیم. برای داشتن همچین قابلیتی میتوانیم Navigation Property را به صورت [NotMapped] تعیین کنیم که EF Core آنها را در نظر نگیرید. بنابراین به صورت دستی عملکرد آنها را پیادهسازی خواهیم کرد. برای اینکار میتوانیم یک اینترفیس با عنوان ICommentable را تعریف کنیم و برای هر مدلی که نیاز به قابلیت کامنت دارد، این اینترفیس را پیادهسازی کنیم. همچنین یک ارجاع به لیستی از کامنتها را به صورت Navigation Property به هر کدام از مدلها نیز اضافه خواهیم کرد:
interface ICommentable { int Id { get; set; } } public class Article : ICommentable { public int Id { get; set; } public string Title { get; set; } public string Slug { get; set; } public string Description { get; set; } [NotMapped] public ICollection<Comment> Comments { get; set; } } public class Video : ICommentable { public int Id { get; set; } public string Url { get; set; } public string Description { get; set; } [NotMapped] public ICollection<Comment> Comments { get; set; } } public class Event : ICommentable { public int Id { get; set; } public string Name { get; set; } public DateTimeOffset? Start { get; set; } public DateTimeOffset? End { get; set; } [NotMapped] public ICollection<Comment> Comments { get; set; } }
سپس درون مدل Comment ارجاع به Polymorphic relation را نیز به صورت [NotMapped] پیادهسازی خواهیم کرد:
public class Comment { public int Id { get; set; } public string CommentText { get; set; } public string User { get; set; } public int? TypeId { get; set; } public CommentType CommentType { get; set; } ICommentable _parent; [NotMapped] public ICommentable Parent { get => _parent; set { _parent = value; TypeId = value.Id; CommentType = (CommentType) Enum.Parse(typeof(CommentType), value.GetType().Name); } } }
کاری که در بالا انجام شده، تنظیم تایپ مدلی است که میخواهیم واکشی کنیم. یعنی به محض مقداردهی، پراپرتی Comments مدل مورد نظر به همراه Id و در نهایت نوع آن را تنظیم کردهایم. اکنون برای واکشی کامنتهای یک مطلب خواهیم داشت:
var article = dbContext.Articles.Find(1); article.Comments = dbContext.Comments .Where(c => c.TypeId == article.Id && c.CommentType == CommentType.Article) .ToList(); foreach (var comment in article.Comments) comment.Parent = article; foreach (var comment in article.Comments) { Console.WriteLine($"{comment.User} - ${comment.CommentText} - {((Article) comment.Parent).Title}"); }
همانطور که مشاهده میکنید اکنون میتوانیم از هر دو طرف رابطه به اطلاعات موردنیازمان دسترسی داشته باشیم.