مباحث تکمیلی مدل‌های خود ارجاع دهنده در EF Code first
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: سه دقیقه

در مورد طراحی Self Referencing Entities پیشتر مطلبی را در این سایت مطالعه کرده‌اید .
یک مثال دیگر آن می‌تواند نظرات چند سطحی در یک سایت باشند. نحوه تعریف آن با مطالبی که در قسمت هشتم عنوان شود تفاوتی نمی‌کند؛ اما ... زمانیکه نوبت به نمایش آن فرا می‌رسد، چند نکته اضافی را باید درنظر گرفت. ابتدا مثال کامل زیر را در نظر بگیرید:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;

namespace EFGeneral
{
    public class BlogComment
    {
        public int Id { set; get; }

        [MaxLength]
        public string Body { set; get; }

        public virtual BlogComment Reply { set; get; }
        public int? ReplyId { get; set; }
        public ICollection<BlogComment> Children { get; set; }
    }

    public class MyContext : DbContext
    {
        public DbSet<BlogComment> BlogComments { get; set; }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Self Referencing Entity
            modelBuilder.Entity<BlogComment>()
                        .HasOptional(x => x.Reply)
                        .WithMany(x => x.Children)
                        .HasForeignKey(x => x.ReplyId)
                        .WillCascadeOnDelete(false);

            base.OnModelCreating(modelBuilder);
        }
    }

    public class Configuration : DbMigrationsConfiguration<MyContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = true;
            AutomaticMigrationDataLossAllowed = true;
        }

        protected override void Seed(MyContext context)
        {
            var comment1 = new BlogComment { Body = "نظر من این است که" };
            var comment12 = new BlogComment { Body = "پاسخی به نظر اول", Reply = comment1 };
            var comment121 = new BlogComment { Body = "پاسخی به پاسخ به نظر اول", Reply = comment12 };

            context.BlogComments.Add(comment121);
            base.Seed(context);
        }
    }

    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());

            using (var ctx = new MyContext())
            {
                var list = ctx.BlogComments
                          //.where ...
                          .ToList() // fills the childs list too
                          .Where(x => x.Reply == null) // for TreeViewHelper                        
                          .ToList();

                if (list.Any())
                {

                }
            }
        }
    }
}

در مثال فوق کلاس نظرات به صورت خود ارجاع دهنده (خاصیت Reply به همین کلاس اشاره می‌کند) تعریف شده است تا کاربران بتوانند تا هر چند سطح لازم، به یک نظر خاص، پاسخ دهند.
در اینجا یک چنین جدولی با اطلاعاتی که ملاحظه می‌کنید تشکیل خواهند شد:


یک نظر ارائه شده و سپس دو نظر تو در توی دیگر برای این نظر ثبت شده است.

اولین نکته اضافه‌‌تری که نسبت به قسمت هشتم قابل ملاحظه است، تعریف خاصیت جدید Children به نحو زیر می‌باشد:
    public class BlogComment
    {
        // ... 
        public ICollection<BlogComment> Children { get; set; }
    }
این خاصیت تاثیری در نحوه تشکیل جدول ندارد. علت تعریف آن به توانمندی EF در پرکردن خودکار آن بر می‌گردد.
اگر به کوئری نوشته شده در متد RunTests دقت کنید، ابتدا یک ToList نوشته شده است. این مورد سبب می‌شود که تمام رکوردهای مرتبط دریافت شوند. مثلا در اینجا سه رکورد دریافت می‌شود. سپس برای اینکه حالت درختی آن حفظ شود، در مرحله بعد ریشه‌ها فیلتر می‌شوند (مواردی که reply آن‌ها null است). سپس این مورد تبدیل به list خواهد شد. اینبار اگر خروجی را بررسی کنیم، به ظاهر فقط یک رکورد است اما ... به نحو زیبایی توسط EF به شکل یک ساختار درختی، بدون نیاز به کدنویسی خاصی، منظم شده است:


سؤال:
برای نمایش این اطلاعات درختی و تو در تو در یک برنامه وب چکار باید کرد؟
تا اینجا که توانستیم اطلاعات را به نحو صحیحی توسط EF مرتب کنیم، برای نمایش آن‌ها در یک برنامه ASP.NET MVC می‌توان از یک TreeViewHelper سورس باز استفاده کرد.
البته کد آن در اصل برای استفاده از EF Code first طراحی نشده و نیاز به اندکی تغییر به نحو زیر دارد تا با EF هماهنگ شود (متد ToList و Count موجود در سورس اصلی آن باید به نحو زیر حذف و اصلاح شوند):
private void AppendChildren(TagBuilder parentTag, T parentItem, Func<T, IEnumerable<T>> childrenProperty)
        {
            var children = childrenProperty(parentItem);
            if (children == null || !children.Any())
            {
                return;
            }
//...

 
  • #
    ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۳:۵۷
    آیا میشه این خاصیت رو اینطور پیاده سازی کرد که:
    public class Person
    {
      // other properties
      
      [Required]
      public virtual Person RelatedPerson {get; set;}
    }
    حالا برای شروع در متد Seed یا معرفی اولین شخص با توجه به این که اینRelatedPerson  نمیتواند خالی باشد چطور میتوان خود شخص را به این پراپرتی معرفی کرد و بعنوان روت اشخاص از آن استفاده کرد و مپ آن به چه شکل است.
    • #
      ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۰۱
      نمی‌شود. ویژگی Required را حذف کنید تا فیلد nullable شود. در مورد مباحث اولیه نگاشت آن به این مطلب مراجعه کنید.
      • #
        ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۰۵
        در مورد NHibernate هم این محدودیت وجود دارد؟
        • #
          ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۰۸
          این محدودیت نیست. کار یک ORM نگاشت اطلاعات کلاس‌های شما به جداول بانک اطلاعاتی است. زمانیکه سمت بانک اطلاعاتی این فیلد باید null پذیر باشد چون ریشه یک درخت تشکیل شده والدی ندارد، سمت کدهای شما هم به همین ترتیب باید تعریف شود تا نگاشت به نحو صحیحی صورت گیرد.

          • #
            ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۲۴
            در مورد تعریف ORM حق با شماست ولیبا اینکه میشه این رکورد رو تو بانک ایجاد کرد، پس شاید راهی برای اینکار در ORM هم باشه، من کلاس رو این شکلی تعریف کردم و مطمئن هستم حین این ذخیره سازی که این رکورد، رکورد اول جدول هم هست مثلا شناسه اون حتما با یک ذخیره میشه.
            • #
              ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۲۷
              اولین رکورد یا به عبارتی ریشه یک درخت، ریشه‌ای ندارد. این ریشه نال رو چطور در بانک اطلاعاتی تعریف و ذخیره می‌کنید؟ بحث ما Id اولین رکورد نیست. بحث کلیدخارجی است که باید نال پذیر باشد و به همین جدول هم اشاره می‌کند و نمایانگر رکورد والد یک رکورد خاص است (جدول خود ارجاع دهنده). در همین مطلب جاری به تصویر اول دقت کنید. بحث ما فیلد ReplyId است که نال پذیر است. این ReplyId کلید خارجی اشاره‌کننده به همین جدول هم هست.
              • #
                ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۳۶
                موضوعی که من میخوام براس تو ORM راهی پیدا کنم مربوط به اینه که بتونم کلاسم رو تغییر ندم و از بتونم فیلد پرنت رو غیر نال تعریف کنم، این اسکریپت رو ببینید:
                CREATE TABLE [dbo].[Tree](
                [Id] [int] IDENTITY(1,1) NOT NULL,
                [Name] [nchar](10) NOT NULL,
                [ParentId] [int] NOT NULL,
                 CONSTRAINT [PK_Tree] PRIMARY KEY CLUSTERED 
                (
                [Id] ASC
                )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
                ) ON [PRIMARY]
                
                GO
                
                ALTER TABLE [dbo].[Tree]  WITH CHECK ADD  CONSTRAINT [FK_Tree_Tree] FOREIGN KEY([ParentId])
                REFERENCES [dbo].[Tree] ([Id])
                GO
                
                ALTER TABLE [dbo].[Tree] CHECK CONSTRAINT [FK_Tree_Tree]
                GO

                حالا برای ایجاد رکورد اولی میدونید که شناسه اون یک هست و برای همین میشه رکورد فیلد [ParentId] رو پر کرد من این امتحان رو انجام دادم و مشکلی پیش نیومد

                • #
                  ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۳۹
                  فیلد parent-id رو دستی مقدار دهی کردی؟ یا با EF مقدار دهی شد؟
                  • #
                    ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۴۵
                    تو این مورد با بانک ولی میگم حتما باید برای سمت ORM هم راهی باشه
                    • #
                      ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۴:۴۷
                      parent-id کلید خارجی است. برای مقدار دهی آن (ثبت اولیه رکورد مرتبط) اگر null پذیر نباشد نیاز است حتما رکورد اشاره کننده به آن وجود خارجی داشته باشد.
                      به همین دلیل باید در این حالت خاص آن‌را نال پذیر تعریف کرد؛ چون رکورد ریشه، والدی ندارد و کلید خارجی آن نال خواهد بود. همچنین در این حالت خاص مورد بحث ما، کلید خارجی به خود جدول جاری اشاره می‌کند (و اگر نال پذیر نباشد کل رکورد ریشه، در بار اول ثبت آن، قابل ذخیره سازی نیست).
                    • #
                      ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۵:۰۷
                      برای درک بهتر این موضوع، سعی کنید دستور زیر را اجرا کنید (از management studio استفاده نکنید):
                      INSERT INTO [Tree]
                                 ([Name]
                                 ,[ParentId])
                           VALUES
                                 ('12'
                                 ,2)
                      قابل ثبت نیست. ضمنا امکان مقدار دهی دستی ParentId هم در اینجا تا زمانیکه رکورد ثبت نشده باشد، میسر نیست (کاری که management studio به صورت دستی انجام داده، چند مرحله کار بوده نه صرفا یک insert معمولی).
                      • #
                        ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۵:۱۵
                        بله ممنون، همین موردی که میگید چند مرحله رو باید بشه با ORM بدست آورد، با SQL Profiler میشه از کارکرد SQL Managment نمونه برداری کرد، البته شاید این کار خروجی مناسبی برای سمت کد نداشته باشه.
                        • #
                          ‫۱۲ سال و ۲ ماه قبل، پنجشنبه ۲۶ مرداد ۱۳۹۱، ساعت ۱۵:۲۰
                          طراحی مدنظر شما اصلا متداول نیست. برای مطالعه بیشتر مراجعه کنید به این مقاله:
                          SQL Server CTE Basics 
                          در اینجا هم ManagerID int NULL تعریف شده.
  • #
    ‫۱۱ سال و ۱۰ ماه قبل، شنبه ۱۸ آذر ۱۳۹۱، ساعت ۰۳:۱۳
    با سلام؛
    اگر ما در مدل یک فیلد به عنوان مثال status داشته بایم و بخواهیم آنهایی که status شان ok است را بر گردانیم 
    var model = _efComment.List(p => p.PostId == postId);
    var list = model.ToList()
    .Where(p => p.ComentStatus == ComentStatus.Ok)
    .Where(x => x.Reply == null)
    .ToList();
    کد بالا جواب نمیده چون اگه پدرش status  باوضعیت ok داشته باشه فرزندانش با هر وضعیتی باش میاره
    راه حلی به نظرتون میاد؟
    • #
      ‫۱۱ سال و ۱۰ ماه قبل، شنبه ۱۸ آذر ۱۳۹۱، ساعت ۰۳:۴۴
      همان ابتدای کار از && برای فیلتر کردن استفاده کنید
      p => p.PostId == postId && p.ComentStatus == ComentStatus.Ok

  • #
    ‫۱۱ سال و ۸ ماه قبل، جمعه ۲۰ بهمن ۱۳۹۱، ساعت ۰۱:۵۹
    با سلام.
    برای تهییه مدل‌های خود ارجاع دهنده، نظرتان در خصوص استفاده از   hierarchyID چیست؟
    • #
      ‫۱۱ سال و ۸ ماه قبل، جمعه ۲۰ بهمن ۱۳۹۱، ساعت ۰۲:۳۶
      قابل انتقال نیست به سایر بانک‌های اطلاعاتی. یکی از اهداف کار با ORMها مستقل کردن برنامه از بانک اطلاعاتی است. مثلا بتونید با SQL CE هم کار کنید که این نوع داده رو پشتیبانی نمی‌کنه و خیلی از بانک‌های اطلاعاتی دیگر نیز به همین ترتیب.
  • #
    ‫۱۱ سال و ۷ ماه قبل، سه‌شنبه ۱ اسفند ۱۳۹۱، ساعت ۲۳:۱۴
    با سلام

    بنده مدل زیر را دارم که مربوط به صفحاتی هستند که والد هم دارند.

    public class Page
        {
            public virtual int Id { get; set; }
            public virtual string Title { get; set; }
            public virtual DateTime? CreatedDate { get; set; }
            public virtual DateTime? ModifiedDate { get; set; }
            public virtual string Body { get; set; }
            public virtual string Keyword { get; set; }
            public virtual string Description { get; set; }
            public virtual string Status { get; set; }
            public virtual bool? CommentStatus { get; set; }
            public virtual int? Order { get; set; }
            public virtual User User { get; set; }
            public virtual User EditedByUser { get; set; }
            public virtual ICollection<Comment> Comments { get; set; }
            public virtual int? ParentId { get; set; }
            public virtual Page Parent { get; set; }
            public virtual ICollection<Page> Children { get; set; }
        }

    و با دستور زیر می‌خواهم از آن کوئری بگیرم:

    this._pages.ToList().Where(page => page.Parent == null).ToList();

    دستور فوق به خوبی کار می‌کنه. ولی وقتی با که دستوراتی که توسط mini-profiler لاگ شده را می‌بینیم که اخطار duplicate reader را می‌دهد.
    برای هر page موجود دستور زیر را صادر می‌کند
    SELECT 
    [Extent1].[Id] AS [Id], 
    [Extent1].[Title] AS [Title], 
    [Extent1].[CreatedDate] AS [CreatedDate], 
    [Extent1].[ModifiedDate] AS [ModifiedDate], 
    [Extent1].[Body] AS [Body], 
    [Extent1].[Keyword] AS [Keyword], 
    [Extent1].[Description] AS [Description], 
    [Extent1].[Status] AS [Status], 
    [Extent1].[CommentStatus] AS [CommentStatus], 
    [Extent1].[Order] AS [Order], 
    [Extent1].[ParentId] AS [ParentId], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[Title] AS [Title1], 
    [Extent2].[CreatedDate] AS [CreatedDate1], 
    [Extent2].[ModifiedDate] AS [ModifiedDate1], 
    [Extent2].[Body] AS [Body1], 
    [Extent2].[Keyword] AS [Keyword1], 
    [Extent2].[Description] AS [Description1], 
    [Extent2].[Status] AS [Status1], 
    [Extent2].[CommentStatus] AS [CommentStatus1], 
    [Extent2].[Order] AS [Order1], 
    [Extent2].[ParentId] AS [ParentId1], 
    [Extent2].[User_Id] AS [User_Id], 
    [Extent2].[EditedByUser_Id] AS [EditedByUser_Id], 
    [Extent1].[User_Id] AS [User_Id1], 
    [Extent1].[EditedByUser_Id] AS [EditedByUser_Id1]
    FROM  [dbo].[Pages] AS [Extent1]
    LEFT OUTER JOIN [dbo].[Pages] AS [Extent2] ON [Extent1].[ParentId] = [Extent2].[Id

    می‌خواستم ببینم کاری میشه کرد تا سربار این کوئری را کمتر کرد؟

    در ضمن اگر بخواهم viewmodel را طوری تعریف کنم تا فیلدهای اضافی مانند createddate و user و... که در هنگام نمایش منوی آبشاری به آنها نیازی ندارم چه کار باید کرد؟ چون من هر کاری کردم نتونستم parent را برای viewmodel به خوبی تعریف کنم.
    ممنون
    • #
      ‫۱۱ سال و ۷ ماه قبل، چهارشنبه ۲ اسفند ۱۳۹۱، ساعت ۰۰:۵۹
      - این خروجی SQL لاگ شده مطلب جاری (با تمام توضیحات و نگاشت‌های آن) توسط برنامه مطمئن SQL Server Profiler است:
      SELECT 
      [Extent1].[Id] AS [Id], 
      [Extent1].[Body] AS [Body], 
      [Extent1].[ReplyId] AS [ReplyId]
      FROM [dbo].[BlogComments] AS [Extent1]
      منطقی هم هست. چون در ToList اول، کار با دیتابیس تمام و قطع می‌شود. ToList دوم سمت کلاینت اجرا می‌شود. یعنی تشکیل درخت نهایی توسط امکانات LINQ to Objects انجام می‌شود و نه هیچ کار اضافه‌ای در سمت سرور.
      - اگر اینجا join اضافی پیدا کردید ... حتما مشکلی در تنظیمات نگاشت‌ها دارید.
      - اگر duplicate reader دارید شاید بخاطر lazy loading سایر خواص راهبری است که تعریف کردید مانند User و EditByUser و غیره. این‌ها اگر قرار است نمایش داده شوند، پیش از ToList اول باید توسط متد الحاقی Include به صورت eager loading تعریف شوند تا lazy loading و duplicate reader نداشته باشید.
      - برای فیلتر فیلدهای اضافی، پیش از ToList اول، با استفاده از Projection و نوشتن یک Select، موارد مورد نیاز را انتخاب کنید.
      • #
        ‫۱۱ سال و ۷ ماه قبل، چهارشنبه ۲ اسفند ۱۳۹۱، ساعت ۰۲:۰۰
        ممنون آقای نصیری

        با راهنمایی شما مشکل Duplicate Reader با نوشتن دستور به شکل زیر حل شد.

        this._pages.Include(page => page.Children).ToList().Where(page => page.Parent == null).ToList();

        الان مشکلی دارم اینه که نمیتونم یه select خوب بنویسم تا فقط مواردی را که میخوام برگردونم.
        مثلا من viewmodel را این شکلی تعریف کردم.

        public class NavBarModel
            {
                public virtual int Id { get; set; }
                public virtual string Title { get; set; }
                public virtual string Status { get; set; }
                public virtual int? Order { get; set; }
                public virtual NavBarModel Parent { get; set; }
                public virtual ICollection<Page> Children { get; set; }
            }

        توی select زدن نمیدونم چه شکلی باید کد بزنم تا parent و children هم یه صورت خودکار پر شوند.

        در حقیقت من می‌خوام این موارد را در یک navigation bar به صورت منوی آبشاری نشون بدم.

        در ضمن شما می‌تونید در نشان دادن اطلاعات فوق به صورت منوی آبشاری با امکان تعریف فرزند تو در تو بدون محدودیت راهنماییم کنید.من کدی واسش نوشتم ولی متاسفانه برای پیاده سازی نامحدود بودن فرزندان منو مشکل دارم.

        ممنون
  • #
    ‫۱۱ سال و ۷ ماه قبل، جمعه ۴ اسفند ۱۳۹۱، ساعت ۱۵:۱۰
    سلام

    راستش من منوهای چند سطحی پویا را برای bootstrap navbar نوشتم. الگوریتمش را مجبور شدم به صورت بازگشتی بنویسم.اگر کسی می‌تونه به صورت غیر بازگشتی بگه ممنونش میشم.
    این کد یه partialpage برای navbar هست که هرکسی برای bootstrap به راحتی میتونه استفاده کنه.

    @model IEnumerable<DomainClasses.Page>
    @helper ShowNavBar(IEnumerable<DomainClasses.Page> pages)
    {
        
        foreach (var page in pages)
        {
            if (page != null)
            {
                if (page.Children.Count == 0)
                {
                    <text><li><a tabindex="-1" href="#">@page.Title</a></li></text>
                }
    
                if (page.Children.Count > 0 && page.Parent == null)
                {         
                    <text><li class="dropdown"><a class="dropdown-toggle" id="dLabel" role="button" data-toggle="dropdown" data-target="#" href="/page.html">@page.Title<b class="caret"></b></a><ul class="dropdown-menu" role="menu" aria-labelledby="dLabel"><li><a tabindex="-1" href="#">@page.Title</a></li></text>
                    @ShowNavBar(page.Children)
                    @:</ul></li>
                }
    
                if (page.Children.Count > 0 && page.Parent != null)
                {         
                    <text><li class="dropdown-submenu"><a tabindex="-1" href="#">@page.Title</a><ul class="dropdown-menu"></text>
                    @ShowNavBar(page.Children)
                    @:</ul></li>
                }
    
            }
        }
        
    }
    <div class="navbar" style="margin-bottom: 10px;">
        <div class="navbar-inner">
            <a class="brand" href="www.google.com">IT-EBOOK</a>
            <ul class="nav">
                <li class="active"><a href="#">خانه</a></li>
                <li><a href="#">ورود</a></li>
                @ShowNavBar(Model)
                <li><a href="#">ارتباط با ما</a></li>
            </ul>
            <div class="input-append pull-left visible-desktop" style="margin-top: 5px;">
                <input class="span6 search-input" id="Text1" type="text">
                <button class="btn btn-primary" type="button">جست و جو</button>
                <button class="btn btn-info btn-advanced-search" type="button">پیشرفته</button>
            </div>
        </div>
    </div>



    الان تنها مشکلم اینه که فیلد‌های اضافی هم کوئری گرفته میشه.میدونم فیلدهای اضافی(بر اساس مدلی که ذکر کردم) را چگونه با استفاده از select حذف کنم اما توی viewmodel نمیدونم چه جوری children را از اطلاعات پر کنم؟
     ممنون


     
    • #
      ‫۱۱ سال و ۵ ماه قبل، جمعه ۱۳ اردیبهشت ۱۳۹۲، ساعت ۰۱:۰۹
      دوست عزیز.
      از مطلب مفید شما متشکرم. من هم مشکل شما رو دارم. آیا به جواب رسیدید؟
  • #
    ‫۱۱ سال و ۶ ماه قبل، چهارشنبه ۱۴ فروردین ۱۳۹۲، ساعت ۱۸:۲۶
    با سلام و تشکر از از این مطلب عالی.
    یک سوال:
    اگر جدول BlogComment با یک یا چند جدول دیگر رابطه داشته باشد (مثلاً جدول کاربر، جدول دسترسی و غیره)، چه راه حلی وجود دارد تا در روش خود ارجاع دهنده سایر رابطه‌ها شرکت نکنند، یعنی فقط تعدادی فیلد از جدول BlogComment انتخاب شود. در حالت فعلی تمام فیلدها از تمام جدولهای مرتبط استخراج میشوند.
    • #
      ‫۱۱ سال و ۶ ماه قبل، چهارشنبه ۱۴ فروردین ۱۳۹۲، ساعت ۱۹:۱۰
      - جدول نظرات اگر با موجودیتی به نام کاربر سر و کار دارد، این خاصیت عموما به صورت virtual تعریف می‌شود یعنی lazy loading قرار است رخ دهد. به عبارتی با ToList اولیه اگر از متدی مانند Include استفاده نشده باشد (برای eager loading اطلاعات وابسته)، به هیچ عنوان خواص lazy بارگذاری نخواهند شد. شاید عنوان کنید که من با استفاده از امکانات دیباگر VS.NET اگر نود مربوط به User رو باز کنم، اطلاعاتش هست. پاسخ این است که بله. دقیقا در همین لحظه که نود رو باز کردید یک کوئری برای دریافت اطلاعات یوزر به سرور ارسال شده و نه پیش از آن. البته می‌شود lazy loading را در EF کلا خاموش کرد یا حتی به صورت مقطعی با استفاده از متدی مانند AsNoTracking. اما حالت پیش فرض دقیقا چیزی است که عنوان شد.
      بنابراین درحالت فعلی تمام فیلدهای وابسته از بانک اطلاعاتی استخراج نمی‌شوند مگر اینکه lazy loading را به نحوی تبدیل به eager loading کرده باشید.
      - ضمنا در حالت فعلی اگر دقت کرده باشید پیش از ToList اول یک سه نقطه گذاشته شده است. یعنی اینجا می‌تونید where بنویسید. می‌تونید Select بنویسید و به صورت اختصاصی یک سری خاصیت مشخص رو انتخاب کنید و خیلی از کارهای دیگر.
      • #
        ‫۱۱ سال و ۶ ماه قبل، چهارشنبه ۱۴ فروردین ۱۳۹۲، ساعت ۱۹:۴۹
        با تشکر فراوان از پاسخ کامل جنابعالی.
        مشکل من از اینجا شروع شد که با Json.Net خواستم نتیجه این کوئری را به Json تبدیل کنم، من دستور زیر را نوشتم، پس از اولین اجرا رکورد اول دو فیلد Body و Id را دارد که کاملاً درست است، اما رکورد بعدی که فرزند رکورد اول است شامل تمام فیلدهای جدول BlogComment و تمام جداول مرتبط با آن است.
        ممنون از شما به خاطر صرف وقت گرانبها.
        var list = context.BlogComment.Where(p => p.UserId == 100)
                        .Select(p => p.Body, p.Id, p.Children)
                        .ToList();


        • #
          ‫۱۱ سال و ۶ ماه قبل، چهارشنبه ۱۴ فروردین ۱۳۹۲، ساعت ۲۰:۴۰
          از ویژگی Newtonsoft.Json.JsonIgnore برای عدم serialization یک خاصیت خاص به JSON استفاده کنید.
          • #
            ‫۱۱ سال و ۶ ماه قبل، پنجشنبه ۱۵ فروردین ۱۳۹۲، ساعت ۰۲:۱۰
            با تشکر فراوان، مشکل من با Newtonsoft.Json.JsonIgnore  حل شد. 
            اما من برای انتخاب تعدادی خصوصیت از کد زیر استفاده کردم، در نتیجه نهایی که در عکس مشخص است، فقط رکورد اول شامل فیلدهای مشخص شده توسط من است، فرزندان این رکورد حاوی تمام فیلدها هستند.
            البته در این کدها از using استفاده نشده چون باعث خطای The ObjectContext instance میشود.
            باز هم ممنون برای اختصاص وقت. 
                public partial class BlogComment
                {
                    public BlogComment()
                    {
                        this.Children = new HashSet<BlogComment>();
                    }
                
                    public int Id { get; set; }
                    public string Body { get; set; }
                    public Nullable<System.DateTime> DateSend { get; set; }
                    public Nullable<System.DateTime> DateRead { get; set; }
                    public bool IsDeleted { get; set; }
                    public int UserId { get; set; }
            
                    [Newtonsoft.Json.JsonIgnore]
                    public virtual BlogComment Reply { get; set; }
            
                    public int? ReplyId { get; set; }
                    public virtual ICollection<BlogComment> Children { get; set; }
                    
                    
                }

            public JsonNetResult Index()
                    {
                        var ctx = new testEntities();
                        var list = ctx.BlogComments
                            .Where(p => p.Id == 1)
                            .Select(p => new
                                           {
                                               p.Id,
                                               p.Body,
                                               p.Children
                                           })
                        .ToList();
            
                        JsonNetResult jsonNetResult = new JsonNetResult();
                        jsonNetResult.Formatting = Formatting.Indented;
            
                        jsonNetResult.SerializerSettings = new JsonSerializerSettings()
                                                               {
                                                                   ReferenceLoopHandling = ReferenceLoopHandling.Ignore
                                                               };
                        jsonNetResult.Data = list;
                        return jsonNetResult;
                    }

            • #
              ‫۱۱ سال و ۶ ماه قبل، پنجشنبه ۱۵ فروردین ۱۳۹۲، ساعت ۰۳:۲۹
              علت این است که p.Children به تمام خواص عمومی شیء BlogComment اشاره می‌کند؛ این رو هم میشه یک سطح دیگر با Projection سبک‌تر کرد و یا بجای Projection در حالت شما ساده‌تر است که JsonIgnore را روی تمام خواصی قرار دهید که نباید توسط JSON.NET بررسی شود. با توجه به lazy loading، این خواص virtual توسط EF در بدو امر بارگذاری نمی‌شوند و همچنین چون توسط JSON.NET به دلیل JsonIgnore معرفی شدن واکاوی مجدد نخواهند شد، بنابراین مشکلی از لحاظ کارآیی یا حجم بالای خروجی نخواهید داشت.
              • #
                ‫۱۱ سال و ۶ ماه قبل، پنجشنبه ۱۵ فروردین ۱۳۹۲، ساعت ۰۴:۰۲
                ممنون از لطف شما.
                عالی و جامع بود.
  • #
    ‫۱۱ سال و ۵ ماه قبل، چهارشنبه ۲۵ اردیبهشت ۱۳۹۲، ساعت ۰۳:۱۶
    باسلام.
    برای مدل‌های خود ارجاع دهنده در هنگام حذف یک رکورد، خطای زیر بوجود می‌آید.
    The DELETE statement conflicted with the SAME TABLE REFERENCE constraint
    چاره کار چیست؟ (می خواهم با حذف یک فولدر تمام محتویات آن نیز حذف شوند.)
  • #
    ‫۱۱ سال و ۳ ماه قبل، دوشنبه ۱۰ تیر ۱۳۹۲، ساعت ۱۶:۰۱
     سلام
     در مثال بالا چون نظرات با یک پست مرتبط هستند قاعدتا براساس یک پست خاص کامنت‌ها رو محدود می‌کنیم. ولی اگه بخواهیم یک کامنت خاص رو با زیر مجموعه هاش دریافت کنیم شرط جستجو رو چطور باید بنویسیم؟
    راهکار زیر در sql  هست که دیتابیس رو به sql server  محدود می‌کنه و معادل لینک هم فکر نکنم داشته باشه.
    DECLARE @Table TABLE(
        ID INT,
        ParentID INT,
        NAME VARCHAR(20)
    )
    
    INSERT INTO @Table (ID,ParentID,[NAME]) SELECT 1, NULL, 'A'
    INSERT INTO @Table (ID,ParentID,[NAME]) SELECT 2, 1, 'B-1'
    INSERT INTO @Table (ID,ParentID,[NAME]) SELECT 3, 1, 'B-2'
    INSERT INTO @Table (ID,ParentID,[NAME]) SELECT 4, 2, 'C-1'
    INSERT INTO @Table (ID,ParentID,[NAME]) SELECT 5, 2, 'C-2'
    
    
    DECLARE @ID INT
    
    SELECT @ID = 2
    
    ;WITH ret AS(
        SELECT*
        FROM@Table
        WHEREID = @ID
        UNION ALL
        SELECTt.*
        FROM@Table t INNER JOIN
        ret r ON t.ParentID = r.ID
    )
    
    SELECT  *
    FROM    ret

    • #
      ‫۱۱ سال و ۳ ماه قبل، دوشنبه ۱۰ تیر ۱۳۹۲، ساعت ۱۸:۲۱
      نیازی نیست زیاد پیچیده فکر کنید. یک لیست ساده رو واکشی کنید (مثلا یک کامنت که سه زیر مجموعه مرتبط دارد با یک select ساده)، اتصال نهایی آن‌ها در سمت کلاینت خودکار است. به عبارتی شکل دهی تو در تو در اینجا در سمت کلاینت انجام می‌شود و نه سمت سرور. سمت سرور آن، یک select معمولی از رکوردهای مورد نظر بیشتر نیست.
      var specifiedCommentId = 4; //عدد 4 در اینجا شماره رکورد کامنتی است که دارای یک سری زیر کامنت است
      var list = ctx.BlogComments
                       .Include(x => x.Reply)
                       .Where(x => x.Id >= specifiedCommentId && (x.ReplyId == x.Reply.Id || x.Reply == null))
                       .ToList() // fills the childs list too
                       .Where(x => x.Reply == null) // for TreeViewHelper                        
                       .ToList();
      • #
        ‫۱۱ سال و ۳ ماه قبل، دوشنبه ۱۰ تیر ۱۳۹۲، ساعت ۲۰:۴۰
        ممنون ولی مشکلی  با سمت کلاینت ندارم مشکل من شرطی هست که بدون واکشی کل سطرها ، خود ارجاعی رو تا هر سطحی که هست پیمایش کنه.
        فکر کنم سوالم رو اینجوری مطرح کنم بهتر باشه، ما تعدادی کارمند داریم که عضو گروههای زیر هستند.گروه بندی خود ارجاع دهنده هست.
        GroupRoot {id=1, Name="گروه اصلی",ParentId=null}
        Group2 {id=2, Name="بازرگانی",ParentId=1}
        Group3 {id=3, Name="فروش",ParentId=2}
        Group4 {id=4, Name="فروش قطعات",ParentId=3}
        Group5 {id=5, Name="خدمات",ParentId=1}
        person1 {name="Ali",  GroupId=2}//بازرگانی
        person2 {name="reza", GroupId=3}//فروش
        person3 {name="iman", GroupId=3}//فروش
        person4 {name="hamid",GroupId=4}//فروش قطعات
        person5 {name="hasan",GroupId=4}//فروش قطعات
        person6 {name="Ahmad",GroupId=5}//خدمات
        person7 {name="vahid",GroupId=1}//گروه اصلی

        گزارش 1: نمایش گروه بازگانی شناسه 2 به همراه زیر مجموعه هاش ؟{بازرگانی،فروش،فروش قطعات}
        گزارش 2 : نمایس پرسنل گروه فروش و زیر مجموعه‌های گروه فروش؟{reza,iman,hamid,hasan}
         
        • #
          ‫۱۱ سال و ۳ ماه قبل، دوشنبه ۱۰ تیر ۱۳۹۲، ساعت ۲۱:۰۳
          کوئری رو که در پاسخ سؤال قبلی شما نوشته شد، یکبار اجرا کنید.
          تمام رکوردها رو واکشی نمی‌کنه. فقط از یک آی دی خاص شروع می‌کنه و مواردی رو که ReplyId اون‌ها با id‌ها یکی است انتخاب می‌کنه. یعنی فقط یک سری زیر خانواده خاص.

          مسایل و پروژه‌های شخصی خودتون رو هم در انجمن‌ها پیگیری کنید.
      • #
        ‫۸ سال و ۱۱ ماه قبل، پنجشنبه ۱۴ آبان ۱۳۹۴، ساعت ۲۲:۰۳
        با تشکر.
        کوئری نوشته شده را توسط DNTProfiler بازبنی کردم و به کوئری T-SQL زیر رسیدم:
        SELECT [Extent1].[Id] AS [Id],
               [Extent1].[Body] AS [Body],
               [Extent1].[ReplyId] AS [ReplyId],
               [Extent2].[Id] AS [Id1],
               [Extent2].[Body] AS [Body1],
               [Extent2].[ReplyId] AS [ReplyId1]
        FROM   [dbo].[BlogComments] AS [Extent1]
               LEFT OUTER JOIN
               [dbo].[BlogComments] AS [Extent2]
               ON [Extent1].[ReplyId] = [Extent2].[Id]
        WHERE  ([Extent1].[Id] >= 2)
               AND (([Extent1].[ReplyId] = [Extent1].[ReplyId])
                    OR (([Extent1].[ReplyId] IS NULL)
                        AND ([Extent1].[ReplyId] IS NULL))
                    OR ([Extent2].[Id] IS NULL));
        این کوئری فقط زیر شاخه‌های نود مورد نظر را واکشی نمیکند بلکه علاوه بر آن نظراتی که زیر شاخه این نود نیستند رو به همراه زیرشاخه هاش ، واکشی میکند.

    • #
      ‫۱۱ سال و ۳ ماه قبل، دوشنبه ۲۴ تیر ۱۳۹۲، ساعت ۱۷:۲۹
      با سلام و عرض تشکر بابت این مطلب کامل

      در خصوص مدل‌های خود ارجاع دهنده، اگر بخواهیم مثل عکس بالا هر نود جدیدی که ثبت میشود مقادیر امتیاز تمام والد‌های آن تا ریشه تغییر کند بهترین راه حل چیست؟
      به بیانی دیگر اگر به شکل دقت کنید هر نود جدیدی که ثبت میشود امتیاز تمامی والدهای آن تا ریشه به روز میشود، برای این کار راه حلی که به نظرم رسید این بود که با ثبت هر نود جدید، تمامی والدهای آن واکشی و مقدار امتیاز آن به روز شوند تا به ریشه برسیم؛ راه حل دیگر نوشتن تریگر در سمت دیتابیس است. فکر میکنم در هر دوحالت سربار زیادی داشته باشیم .آیا راه بهتری وجود دارد؟
      ممنون
      • #
        ‫۱۱ سال و ۳ ماه قبل، دوشنبه ۲۴ تیر ۱۳۹۲، ساعت ۱۸:۳۷
        کم هزینه‌ترین روش: جمع‌ها رو سمت کلاینت مدیریت کنید و اصلا اطلاعات جمع آن‌ها رو سمت سرور ثبت نکنید. زمانیکه این TreeView در حال رندر هست، جمع گره‌های والد رو بر اساس فرزندان محاسبه و نمایش بدید.
        • #
          ‫۱۱ سال و ۳ ماه قبل، دوشنبه ۲۴ تیر ۱۳۹۲، ساعت ۱۸:۵۳
          بسیار ممنونم از پاسخ شما.
          روش شما برای زمانی که تمام درخت لود شود و به کلاینت فرستاده شود کاملاً عالی است. اما اگر با Ajax مرحله به مرحله نودها لود شود دیگر این روش پاسخ نمیدهد چون پدر از امتیازات تمامی فرزندانش بی خبر است. 
          باز هم ممنون
          • #
            ‫۱۱ سال و ۳ ماه قبل، دوشنبه ۲۴ تیر ۱۳۹۲، ساعت ۱۹:۰۲
            برای نمایش اول کار: جمع کل ریشه اصلی مساوی است با جمع فرزندانی که والد غیر null دارند. یک کوئری سبک بیشتر نیست. مابقی جمع‌های درخت رو میشه سمت کلاینت مدیریت کرد.
  • #
    ‫۱۱ سال و ۲ ماه قبل، دوشنبه ۷ مرداد ۱۳۹۲، ساعت ۱۷:۵۲
    روش بهینه‌ی حذف پدر و فرزندان اون چیه؟! ... ممنون میشم اگه رو همین مثال لطف کنین
    • #
      ‫۱۱ سال و ۲ ماه قبل، دوشنبه ۷ مرداد ۱۳۹۲، ساعت ۱۸:۰۰
      - نمی‌تونید والد رو یک ضرب حذف کنید. از آخرین فرزند به اول ریشه (والد ریشه جاری) باید (یکی یکی) حذف کنید تا قیود تعریف شده مانع حذف اطلاعات نشوند.
      - یا اینکه ... soft delete انجام بدید. یک فیلد bool تعریف کنید که این رکورد خاص deleted هست یا خیر.
    • #
      ‫۱۱ سال و ۲ ماه قبل، دوشنبه ۷ مرداد ۱۳۹۲، ساعت ۱۸:۰۲
      من از این استفاده می‌کنم.(مدل کامنت و پاسخ هاش)
              public void Remove(int id)
              {
                  var selectedComment = _comments.Find(id);
                  _comments.Where(x => x.ParentId == id).Load();
                  _comments.Remove(selectedComment);
              }

      • #
        ‫۱۱ سال و ۲ ماه قبل، دوشنبه ۷ مرداد ۱۳۹۲، ساعت ۱۸:۵۳
        روش شما رو تست کردم ولی جواب نداد!... فکر میکنم این ساختاری که شما تعریف کردید برای حالت cascadeDelete جواب میده ... 
        طبق فرمایش آقای نصیری کاری که من کردم اینه (روی سیستم گروه و زیرگروه) : 
        1 - واکشی گروه انتخاب شده و فرزندان آن با استفاده از الگوریتم اول سطح : 
        private Stack<Group> GetChildsAndRoot(Group group)
                {
                    var stack = new Stack<Group>();
                    var queue = new Queue<Group>();
                    stack.Push(group);
                    queue.Enqueue(group);
                    while (queue.Any())
                    {
                        var currGroup = queue.Dequeue();
                        foreach (var child in currGroup.Childs)
                        {
                            queue.Enqueue(child);
                            stack.Push(child);
                        }
                    }
        
                    return stack;
                }
        2- پاک کردن پشته‌ی بازگشتی : 
        var group = _groupRepository.GetByID(id);
        
        var nodes = GetChildsAndRoot(group);
        while (nodes.Any())
        {
                _groupRepository.Delete(nodes.Pop());
        }
                        
             _unitOfWork.SaveChanges();

        • #
          ‫۱۱ سال و ۲ ماه قبل، دوشنبه ۷ مرداد ۱۳۹۲، ساعت ۱۹:۴۶
          نه کار میده. اون هم در حالتی که cascade نباشه.
          این کدی که نوشتم دقیقا مال پروژه‌ی IRIS هست. یک بار کدش را در اون پروژه ببینید؛ ضرری نداره.
  • #
    ‫۱۰ سال و ۴ ماه قبل، شنبه ۱۷ خرداد ۱۳۹۳، ساعت ۱۵:۴۸
    با سلام.
    چگونه می‌توان childها را نیز بر اساس فیلد دلخواه مرتب سازی کرد. برای مثال کوئری زیر تنها سطح اول را مرتب می‌کند و فرزندان را مرتب نمیکند.
    var query = _tEntities.AsNoTracking()
                    .Where(p => p.Parent_Id == null && p.IsActive == true)
                    .OrderBy(sortExpression);
    var result = query.ToList();
    return result;

  • #
    ‫۹ سال و ۷ ماه قبل، دوشنبه ۴ اسفند ۱۳۹۳، ساعت ۰۰:۰۹
    سلام
    - شما برای نمایش نظرات چند سطحی سایت جاری چه روشی را بکار برده اید و پیشنهاد  می‌کنید (در پروژه‌های MVC و EF CF) ؟
    - استفاده از Recursive CTE برای نمایش نظرات چند سطحی روش مناسبیه ؟
     ممنونم
    • #
      ‫۹ سال و ۷ ماه قبل، دوشنبه ۴ اسفند ۱۳۹۳، ساعت ۰۰:۱۴
      - مطلب جاری دقیقا خلاصه‌ی کاری است که انجام شده؛ به همراه روش نمایشی آن که در انتهای بحث ذکر شد.
      - بله. البته اگر بخواهید مستقیما SQL بنویسید، دیگر نیازی به ORMها نخواهد بود و خیلی از قابلیت‌های بومی دیتابیس‌ها امکان انتقال ندارند و پروژه را وابسته به یک دیتابیس خاص می‌کنند.
  • #
    ‫۹ سال و ۳ ماه قبل، یکشنبه ۱۴ تیر ۱۳۹۴، ساعت ۰۲:۰۲
    دلیل فراخوانی OnModelCraeting در کلاس base چی هست؟
    base.OnModelCreating(modelBuilder);
    یا بهتر بگم که چه موقع باید فراخوانی بشه و چه موقع نشه؟
    • #
      ‫۹ سال و ۳ ماه قبل، یکشنبه ۱۴ تیر ۱۳۹۴، ساعت ۰۲:۲۹
      ممکن است در کلاس پایه، تنظیمات پیش فرضی وجود داشته باشند. این فراخوانی، این تنظیمات را حفظ خواهد کرد. برای مثال در ASP.NET Identity، در کلاس پایه Context آن، یک سری تنظیمات پیش فرض نام جداول، ایندکس‌ها و روابط هست. اگر این فراخوانی صورت نگیرد، تمام آن‌ها را از دست خواهید داد.
  • #
    ‫۸ سال و ۷ ماه قبل، دوشنبه ۱۰ اسفند ۱۳۹۴، ساعت ۰۲:۱۵
    با تشکر؛ آقای نصیری مشکلی که آقای سعیدی فر به آن اشاره کردند را بنده با AutoMapper هم بررسی کردم ولی جوابی نگرفتم . دلیل این مورد چیست؟ آیا تخریب شدن داینامیک پراکسی باعث این موضوع میشود؟ چون بررسی کردم در هنگام استفاده از ویومدل ، داینامیک پراکسی ای که در هنگام استفاده مستقیم از کلاس دامین ایجاد میشود ، تخریب شده است.
  • #
    ‫۱ سال و ۲ ماه قبل، دوشنبه ۲۲ خرداد ۱۴۰۲، ساعت ۱۲:۱۴
    به دلیل اینکه دیگر در EF Core متدهای HasOptional و WillCascadeOnDelete را نداریم قسمت ModelBuilder به شکل زیر تغییر می‌کند.
    modelBuilder.Entity<BlogComment>()
                            .HasOne(x => x.Reply)
                            .WithMany(x => x.Children)
                            .HasForeignKey(x => x.ReplyId)
                            .OnDelete(DeleteBehavior.SetNull);
    فقط بایستی navigation‌های مربوطه هم نال پذیر باشد یعنی:
      public virtual BlogComment? Reply { set; get; }
    public int? ReplyId { get; set; }
    public ICollection<BlogComment>? Children { get; set; }