شروع به کار با EF Core 1.0 - قسمت 11 - بررسی رابطهی Self Referencing
نویسنده: وحید نصیری
تاریخ: ۱۳۹۵/۰۶/۰۹ ۱۴:۳۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
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; } 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);
});
} 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();
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
var menuesFirst = await _publicMenus.Where(p => p.Language == _caltureName).Cacheable().ToListAsync(); var menues = menuesFirst.Where(x => x.MenuId == null).ToList();
ساختار موجودیت در DomainLayer :
public class PublicMenu : BaseEntity
{
public PublicMenu()
{
SubMenus = new List<PublicMenu>();
}
public string Title { get; set; }
public string Url { get; set; }
public string Icon { get; set; }
public bool IsShow { get; set; }
public virtual PublicMenu Menu { set; get; }
public int? MenuId { get; set; }
public ICollection<PublicMenu> SubMenus { get; set; }
}
void buildTreeLinqToObjects()
{
var comment1 = new BlogComment { Id = 1, Body = "نظر من این است که" };
var comment12 = new BlogComment { Id = 2, Body = "پاسخی به نظر اول", ReplyId = 1 };
var comment121 = new BlogComment { Id = 3, Body = "پاسخی به پاسخ به نظر اول", ReplyId = 2 };
var comment2 = new BlogComment { Id = 4, Body = "نظر من این بود که" };
var comment22 = new BlogComment { Id = 5, Body = "پاسخی به نظر قبلی", ReplyId = 4 };
var comment221 = new BlogComment { Id = 6, Body = "پاسخی به پاسخ به نظر من اول", ReplyId = 5 };
var list = new List<BlogComment>
{
comment1, comment12, comment121,
comment2, comment22, comment221
};
foreach (var item in list)
{
if (item.ReplyId == null)
{
continue;
}
var parent = list.First(x => x.Id == item.ReplyId.Value);
parent.Children.Add(item);
}
} public class EventItem
{
public int Id { get; set; }
public int? ParentId { get; set; }
public virtual EventItem Parent { get; set; }
public virtual EventItem Alternative { get; set; }
} public class EventItemConfiguration : IEntityTypeConfiguration<EventItem>
{
public void Configure(EntityTypeBuilder<EventItem> builder)
{
builder.HasIndex(e => e.ParentId);
builder.HasOne(e => e.Parent)
.WithOne(e => e.Alternative)
.HasForeignKey<EventItem>(e => e.ParentId);
}
} public class Category
{
public int Id { get; set; }
[StringLength(450)]
public string Name { get; set; }
public int? ParentId { get; set; }
public virtual Category Parent { get; set; }
public virtual ICollection<Category> Children { get; set; }
}
public class CategoryConfiguration : IEntityTypeConfiguration<Category>
{
public void Configure(EntityTypeBuilder<Category> builder)
{
builder.HasIndex(c => c.ParentId);
builder.HasOne(c => c.Parent)
.WithMany(c => c.Children)
.HasForeignKey(c => c.ParentId);
}
} Introducing FOREIGN KEY constraint 'FK_Categories_Categories_ParentId' on table 'Categories' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints. Could not create constraint or index. See previous errors.
var comments = _dbContext.Comments.Where(x => x.ReplyId == null).Include(x => x.Children).ToList();
یک نکتهی تکمیلی: تاثیر فراخوانی متد AsNoTracking بر روی کوئریهای خود ارجاعی
همانطور که در مطلب «مباحث تکمیلی مدلهای خود ارجاع دهنده در EF Code first» هم مشاهده کردید، خود EF، قابلیت تشکیل درخت نهایی خود ارجاع دهنده را دارد و به این ترتیب کوئری گرفتن از نتیجهی آن، بسیار ساده میشود. اما ... اگر در این بین، از متد AsNoTracking برای بهینه سازی، کاهش میزان حافظه و حذف پروکسیهای ردیابی تغییرات EF استفاده شود، دیگر این درخت خودکار، تشکیل نخواهد شد. برای پوشش این حالت میتوان به صورت زیر عمل کرد:
الف) تشکیل یک کلاس پایه برای تعریف سادهتر و مشخص رابطههای خود ارجاعی
public abstract class BaseEntity
{
public int Id { get; set; }
}
public abstract class BaseSelfReferencingEntity<TSelfEntity> : BaseEntity
where TSelfEntity : BaseEntity
{
public virtual TSelfEntity? Reply { set; get; }
public int? ReplyId { get; set; }
public virtual ICollection<TSelfEntity>? Children { get; set; }
}که ساختار معرفی شدهی در اینجا، با توضیحات موجود در متن، انطباق دارد.
ب) پر کردن درخت نهایی حاصل به صورت دستی:
چون دیگر EF این درخت را برای ما تشکیل نمیدهد، اکنون باید خودمان کار تشکیل آنرا به صورت زیر انجام دهیم:
public static class SelfReferencingExtensions
{
public static List<TEntity> ToSelfReferencingTree<TEntity>(this ICollection<TEntity>? originalList)
where TEntity : BaseSelfReferencingEntity<TEntity>
{
var results = new List<TEntity>();
if (originalList is null || originalList.Count == 0)
{
return results;
}
foreach (var rootItem in originalList.Where(x => !x.ReplyId.HasValue))
{
results.Add(rootItem);
AppendChildren(originalList, rootItem);
}
return results;
}
private static void AppendChildren<TEntity>(ICollection<TEntity> originalList, TEntity parentItem)
where TEntity : BaseSelfReferencingEntity<TEntity>
{
foreach (var kid in originalList.Where(x => x.ReplyId.HasValue && x.ReplyId.Value == parentItem.Id))
{
parentItem.Children ??= new List<TEntity>();
parentItem.Children.Add(kid);
AppendChildren(originalList, kid);
}
}
}در اینجا کار تشکیل درخت نهایی، با استفاده از یک متد بازگشتی، انجام میشود.
پس از این مقدمات، نحوهی استفاده از آن به صورت زیر است:
var comments = await _comments.AsNoTracking()
.Where(x => x.ParentId == postId)
.OrderBy(x => x.Id)
.Take(count)
.ToListAsync();
var commentsTree = comments.ToSelfReferencingTree();کوئری نویسی ابتدایی آن، کاملا استاندارد و بدون هیچگونه نکتهی خاصی است. ابتدا تمام نظرات یک مطلب (به صورت AsNoTracking) بازگشت داده میشوند و سپس متد ToSelfReferencingTree کار اتصالات نهایی درخت پاسخها را به صورت خودکار انجام میدهد.
آیا متد AsNoTrackingWithIdentityResolution را تست کردید که جواب میده یا نه؟
با اجرای این کوئری خاص به همراه AsNoTrackingWithIdentityResolution خطای زیر مشاهده میشود و اصلا کار نمیکند (چون فیلد JSON هم دارد):
System.InvalidOperationException: Invalid token type: 'StartObject'.
در EF-Core و در طی طول عمر «یک» Context:
بنابراین وجود AsNoTrackingWithIdentityResolution فقط در طی طول عمر یک Conetxt و برای کاهش سربار نمونه سازی اشیاء مشابه با IDهای یکسان کوئریهای آن Context ارزشمند است و اگر Context شما از چندین کوئری مشابه تشکیل نمیشود، عملا کار بیشتری را انجام نمیدهد و تفاوتی با AsNoTracking ندارد.