رابطه چند به چند در مطالب EF Code first سایت جاری، در حدتعریف نگاشتهای آنبررسی شده، اما نیاز به جزئیات بیشتری برای کار با آن وجود دارد که در ادامه به بررسی آنها خواهیم پرداخت:
1) پیش فرضهای EF Code first در تشخیص روابط چند به چند
تشخیص اولیه روابط چند به چند، مانند یک مطلب موجود در سایت و برچسبهای آن؛ که در این حالت یک برچسب میتواند به چندین مطلب مختلف اشاره کند و یا برعکس، هر مطلب میتواند چندین برچسب داشته باشد، نیازی به تنظیمات خاصی ندارد. همینقدر که دو طرف رابطه توسط یک ICollection به یکدیگر اشاره کنند، مابقی مسایل توسط EF Code first به صورت خودکار حل و فصل خواهند شد:
در این مثال، رابطه بین مطالب ارسالی در یک سایت و برچسبهای آن به صورت many-to-many تعریف شده است و همینقدر که دو طرف رابطه توسط یک ICollection به هم اشاره میکنند، رابطه زیر تشکیل خواهد شد:
در اینجا تمام تنظیمات صورت گرفته بر اساس یک سری از پیش فرضها است. برای مثال نام جدول واسط تشکیل شده، بر اساس تنظیم پیش فرض کنار هم قرار دادن نام دو جدول مرتبط تهیه شده است.
همچنین بهتر است بر روی نام برچسبها، یک ایندکس منحصربفرد نیز تعیین شود: (^و ^)
2) تنظیم ریز جزئیات روابط چند به چند در EF Code first
تنظیمات پیش فرض انجام شده آنچنان نیازی به تغییر ندارند و منطقی به نظر میرسند. اما اگر به هر دلیلی نیاز داشتید کنترل بیشتری بر روی جزئیات این مسایل داشته باشید، باید از Fluent API جهت اعمال آنها استفاده کرد:
در اینجا توسط متد Map، نام کلیدهای تعریف شده و همچنین جدول واسط تغییر داده شدهاند:
3) حذف اطلاعات چند به چند
برای حذف تگهای یک مطلب، کافی است تک تک آنها را یافته و توسط متد Remove جهت حذف علامتگذاری کنیم. نهایتا با فراخوانی متد SaveChanges، حذف نهایی انجام و اعمال خواهد شد.
در اینجا تنها اتفاقی که رخ میدهد، حذف اطلاعات ثبت شده در جدول واسط BlogPostsJoinTags است. Tag1 ثبت شده در متد Seed فوق، حذف نخواهد شد. به عبارتی اطلاعات جداول Tags و BlogPosts بدون تغییر باقی خواهند ماند. فقط یک رابطه بین آنها که در جدول واسط تعریف شده است، حذف میگردد.
در ادامه اینبار اگر خود post1 را حذف کنیم:
علاوه بر حذف post1، رابطه تعریف شده آن در جدول BlogPostsJoinTags نیز حذف میگردد؛ اما Tag1 حذف نخواهد شد.
بنابراین دراینجا cascade delete ایی که به صورت پیش فرض وجود دارد، تنها به معنای حذف تمامی ارتباطات موجود در جدول میانی است و نه حذف کامل طرف دوم رابطه. اگر مطلبی حذف شد، فقط آن مطلب و روابط برچسبهای متعلق به آن از جدول میانی حذف میشوند و نه برچسبهای تعریف شده برای آن.
البته این تصمیم هم منطقی است. از این لحاظ که اگر قرار بود دو طرف یک رابطه چند به چند با هم حذف شوند، ممکن بود با حذف یک مطلب، کل بانک اطلاعاتی خالی شود! فرض کنید یک مطلب دارای سه برچسب است. این سه برچسب با 20 مطلب دیگر هم رابطه دارند. اکنون مطلب اول را حذف میکنیم. برچسبهای متناظر آن نیز باید حذف شوند. با حذف این برچسبها طرف دوم رابطه آنها که چندین مطلب دیگر است نیز باید حذف شوند!
4) ویرایش و یا افزودن اطلاعات چند به چند
در مثال فوق فرض کنید که میخواهیم به اولین مطلب ثبت شده، تعدادی تگ جدید را اضافه کنیم:
در اینجا به صورت خودکار، ابتدا tag2 ذخیره شده و سپس ارتباط آن با post1 در جدول رابط ذخیره خواهد شد.
در مثالی دیگر اگر یک برنامه ASP.NET را درنظر بگیریم، در هربار ویرایش یک مطلب، تعدادی Tag به سرور ارسال میشوند. در ابتدای امر هم مشخص نیست کدامیک جدید هستند، چه تعدادی در لیست تگهای قبلی مطلب وجود دارند، یا اینکه کلا از لیست برچسبها حذف شدهاند:
در این مثال فقط تعدادی رشته از کاربر دریافت شده است، بدون Id آنها. ابتدا مطلب متناظر، به همراه تگهای آن توسط متد Include دریافت میشود. سپس نیاز داریم به سیستم ردیابی EF اعلام کنیم که اتفاقاتی قرار است رخ دهد. به همین جهت تمام تگهای مطلب یافت شده را خالی خواهیم کرد. سپس در یک کوئری، بر اساس نام تگهای دریافتی، معادل آنها را از بانک اطلاعاتی دریافت خواهیم کرد؛ کوئری tagsList.Contains به where in در طی یک رفت و برگشت، ترجمه میشود:
آنهایی که جدید هستند به بانک اطلاعاتی اضافه شده (بدون نیاز به تعریف قبلی آنها)، آنهایی که در لیست قبلی برچسبهای مطلب بودهاند، حفظ خواهند شد.
لازم است لیست موارد موجود را (listOfActualTags) از بانک اطلاعاتی دریافت کنیم، زیرا به این ترتیب سیستم ردیابی EF آنها را به عنوان رکوردی جدید و تکراری ثبت نخواهد کرد.
5) تهیه کوئریهای LINQ بر روی روابط چند به چند
الف) دریافت یک مطلب خاص به همراه تمام تگهای آن:
ب) دریافت کلیه مطالبی که شامل Tag1 هستند:
و یا :
1) پیش فرضهای EF Code first در تشخیص روابط چند به چند
تشخیص اولیه روابط چند به چند، مانند یک مطلب موجود در سایت و برچسبهای آن؛ که در این حالت یک برچسب میتواند به چندین مطلب مختلف اشاره کند و یا برعکس، هر مطلب میتواند چندین برچسب داشته باشد، نیازی به تنظیمات خاصی ندارد. همینقدر که دو طرف رابطه توسط یک ICollection به یکدیگر اشاره کنند، مابقی مسایل توسط EF Code first به صورت خودکار حل و فصل خواهند شد:
using System; using System.Linq; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Data.Entity.ModelConfiguration; namespace Sample { public class BlogPost { public int Id { set; get; } [StringLength(maximumLength: 450, MinimumLength = 1), Required] public string Title { set; get; } [MaxLength] public string Body { set; get; } public virtual ICollection<Tag> Tags { set; get; } // many-to-many public BlogPost() { Tags = new List<Tag>(); } } public class Tag { public int Id { set; get; } [StringLength(maximumLength: 450), Required] public string Name { set; get; } public virtual ICollection<BlogPost> BlogPosts { set; get; } // many-to-many public Tag() { BlogPosts = new List<BlogPost>(); } } public class MyContext : DbContext { public DbSet<BlogPost> BlogPosts { get; set; } public DbSet<Tag> Tags { get; set; } } public class Configuration : DbMigrationsConfiguration<MyContext> { public Configuration() { AutomaticMigrationsEnabled = true; AutomaticMigrationDataLossAllowed = true; } protected override void Seed(MyContext context) { var tag1 = new Tag { Name = "Tag1" }; context.Tags.Add(tag1); var post1 = new BlogPost { Title = "Title...1", Body = "Body...1" }; context.BlogPosts.Add(post1); post1.Tags.Add(tag1); base.Seed(context); } } public static class Test { public static void RunTests() { Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>()); using (var ctx = new MyContext()) { var post1 = ctx.BlogPosts.Find(1); if (post1 != null) { Console.WriteLine(post1.Title); } } } } }
در اینجا تمام تنظیمات صورت گرفته بر اساس یک سری از پیش فرضها است. برای مثال نام جدول واسط تشکیل شده، بر اساس تنظیم پیش فرض کنار هم قرار دادن نام دو جدول مرتبط تهیه شده است.
همچنین بهتر است بر روی نام برچسبها، یک ایندکس منحصربفرد نیز تعیین شود: (^و ^)
2) تنظیم ریز جزئیات روابط چند به چند در EF Code first
تنظیمات پیش فرض انجام شده آنچنان نیازی به تغییر ندارند و منطقی به نظر میرسند. اما اگر به هر دلیلی نیاز داشتید کنترل بیشتری بر روی جزئیات این مسایل داشته باشید، باید از Fluent API جهت اعمال آنها استفاده کرد:
public class TagMap : EntityTypeConfiguration<Tag> { public TagMap() { this.HasMany(x => x.BlogPosts) .WithMany(x => x.Tags) .Map(map => { map.MapLeftKey("TagId"); map.MapRightKey("BlogPostId"); map.ToTable("BlogPostsJoinTags"); }); } } public class MyContext : DbContext { public DbSet<BlogPost> BlogPosts { get; set; } public DbSet<Tag> Tags { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new TagMap()); base.OnModelCreating(modelBuilder); } }
3) حذف اطلاعات چند به چند
برای حذف تگهای یک مطلب، کافی است تک تک آنها را یافته و توسط متد Remove جهت حذف علامتگذاری کنیم. نهایتا با فراخوانی متد SaveChanges، حذف نهایی انجام و اعمال خواهد شد.
using (var ctx = new MyContext()) { var post1 = ctx.BlogPosts.Find(1); if (post1 != null) { Console.WriteLine(post1.Title); foreach (var tag in post1.Tags.ToList()) post1.Tags.Remove(tag); ctx.SaveChanges(); } }
در ادامه اینبار اگر خود post1 را حذف کنیم:
var post1 = ctx.BlogPosts.Find(1); if (post1 != null) { ctx.BlogPosts.Remove(post1); ctx.SaveChanges(); }
بنابراین دراینجا cascade delete ایی که به صورت پیش فرض وجود دارد، تنها به معنای حذف تمامی ارتباطات موجود در جدول میانی است و نه حذف کامل طرف دوم رابطه. اگر مطلبی حذف شد، فقط آن مطلب و روابط برچسبهای متعلق به آن از جدول میانی حذف میشوند و نه برچسبهای تعریف شده برای آن.
البته این تصمیم هم منطقی است. از این لحاظ که اگر قرار بود دو طرف یک رابطه چند به چند با هم حذف شوند، ممکن بود با حذف یک مطلب، کل بانک اطلاعاتی خالی شود! فرض کنید یک مطلب دارای سه برچسب است. این سه برچسب با 20 مطلب دیگر هم رابطه دارند. اکنون مطلب اول را حذف میکنیم. برچسبهای متناظر آن نیز باید حذف شوند. با حذف این برچسبها طرف دوم رابطه آنها که چندین مطلب دیگر است نیز باید حذف شوند!
4) ویرایش و یا افزودن اطلاعات چند به چند
در مثال فوق فرض کنید که میخواهیم به اولین مطلب ثبت شده، تعدادی تگ جدید را اضافه کنیم:
var post1 = ctx.BlogPosts.Find(1); if (post1 != null) { var tag2 = new Tag { Name = "Tag2" }; post1.Tags.Add(tag2); ctx.SaveChanges(); }
در مثالی دیگر اگر یک برنامه ASP.NET را درنظر بگیریم، در هربار ویرایش یک مطلب، تعدادی Tag به سرور ارسال میشوند. در ابتدای امر هم مشخص نیست کدامیک جدید هستند، چه تعدادی در لیست تگهای قبلی مطلب وجود دارند، یا اینکه کلا از لیست برچسبها حذف شدهاند:
//نام تگهای دریافتی از کاربر var tagsList = new[] { "Tag1", "Tag2", "Tag3" }; //بارگذاری یک مطلب به همراه تگهای آن var post1 = ctx.BlogPosts.Include(x => x.Tags).FirstOrDefault(x => x.Id == 1); if (post1 != null) { //ابتدا کلیه تگهای موجود را حذف خواهیم کرد if (post1.Tags != null && post1.Tags.Any()) post1.Tags.Clear(); //سپس در طی فقط یک کوئری بررسی میکنیم کدامیک از موارد ارسالی موجود هستند var listOfActualTags = ctx.Tags.Where(x => tagsList.Contains(x.Name)).ToList(); var listOfActualTagNames = listOfActualTags.Select(x => x.Name.ToLower()).ToList(); //فقط موارد جدید به تگها و ارتباطات موجود اضافه میشوند foreach (var tag in tagsList) { if (!listOfActualTagNames.Contains(tag.ToLowerInvariant().Trim())) { ctx.Tags.Add(new Tag { Name = tag.Trim() }); } } ctx.SaveChanges(); // ثبت موارد جدید //موارد قبلی هم حفظ میشوند foreach (var item in listOfActualTags) { post1.Tags.Add(item); } ctx.SaveChanges(); }
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[Tags] AS [Extent1] WHERE [Extent1].[Name] IN (N'Tag1',N'Tag2',N'Tag3')
لازم است لیست موارد موجود را (listOfActualTags) از بانک اطلاعاتی دریافت کنیم، زیرا به این ترتیب سیستم ردیابی EF آنها را به عنوان رکوردی جدید و تکراری ثبت نخواهد کرد.
5) تهیه کوئریهای LINQ بر روی روابط چند به چند
الف) دریافت یک مطلب خاص به همراه تمام تگهای آن:
ctx.BlogPosts.Where(p => p.Id == 1).Include(p => p.Tags).FirstOrDefault()
var posts = from p in ctx.BlogPosts from t in p.Tags where t.Name == "Tag1" select p;
var posts = ctx.Tags.Where(x => x.Name == "Tag1").SelectMany(x => x.BlogPosts);