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

گاهی از اوقات یافتن معادل LINQ کوئری‌های SQLایی که پیشتر به سادگی و بر اساس ممارست، در کسری از دقیقه نوشته می‌شدند، آنچنان ساده نیست. برای مثال فرض کنید یک سری پروژه وجود دارند که به ازای هر پروژه، تعدادی بازخورد ثبت شده است. هر بازخورد نیز دارای وضعیت‌هایی مانند «در حال انجام» و «انجام شد» است. می‌خواهیم کوئری LINQ سازگار با EF ایی را تهیه کنیم که تعداد موارد «در حال انجام» را نمایش دهد.
بر این اساس، کلاس‌های مدل دومین مساله به صورت زیر خواهند بود:
    public class Project
    {
        public int Id { set; get; }
        public string Name { set; get; }

        public virtual ICollection<ProjectIssue> ProjectIssues { set; get; }
    }

    public class ProjectIssue
    {
        public int Id { set; get; }
        public string Body { set; get; }

        [ForeignKey("ProjectStatusId")]
        public virtual ProjectIssueStatus ProjectIssueStatus { set; get; }
        public int ProjectStatusId { set; get; }

        [ForeignKey("ProjectId")]
        public virtual Project Project { set; get; }
        public int ProjectId { set; get; }
    }

    public class ProjectIssueStatus
    {
        public int Id { set; get; }
        public string Name { set; get; }

        public virtual ICollection<ProjectIssue> ProjectIssues { set; get; }
    }
یک پروژه می‌تواند تعدادی Issue ثبت شده داشته باشد. هر Issue نیز دارای وضعیتی مشخص است.
اگر EF Code first را وادار به تهیه جداول و روابط معادل کلاس‌های فوق کنیم:
    public class MyContext : DbContext
    {
        public DbSet<ProjectIssueStatus> ProjectStatus { get; set; }
        public DbSet<ProjectIssue> ProjectIssues { get; set; }
        public DbSet<Project> Projects { get; set; }
    }

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

        protected override void Seed(MyContext context)
        {
            var project1 = new Project { Name = "پروژه جدید" };
            context.Projects.Add(project1);

            var stat1 = new ProjectIssueStatus { Name = "درحال انجام" };
            var stat2 = new ProjectIssueStatus { Name = "انجام شد" };
            context.ProjectStatus.Add(stat1);
            context.ProjectStatus.Add(stat2);

            var issue1 = new ProjectIssue
            {
                Body = "تغییر قلم گزارش",
                ProjectIssueStatus = stat1,
                Project = project1
            };
            var issue2 = new ProjectIssue
            {
                Body = "تغییر لوگوی گزارش",
                ProjectIssueStatus = stat1,
                Project = project1
            };
            context.ProjectIssues.Add(issue1);
            context.ProjectIssues.Add(issue2);

            base.Seed(context);
        }
    }
 به شکل زیر خواهیم رسید:


سابقا برای یافتن تعداد متناظر با هر IssueStatus خیلی سریع می‌شد چنین کوئری را نوشت:


اما اکنون معادل آن با EF Code first چیست؟
    public static class Test
    {
        public static void RunTests()
        {
            Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());

            using (var ctx = new MyContext())
            {
                var projectId = 1;
                var list = ctx.ProjectStatus.Select(x => new
                                                    {
                                                        Id = x.Id,
                                                        Name = x.Name,
                                                        Count = x.ProjectIssues.Count(p => p.ProjectId == projectId)
                                                    }).ToList();
                foreach (var item in list)
                    Console.WriteLine("{0}:{1}",item.Name, item.Count);                
            }
        }
    }
بله. همانطور که ملاحظه می‌کنید در اینجا به کوئری بسیار ساده و واضحی با کمک استفاده از navigation properties (خواص راهبری مانند ProjectIssues) تعریف شده رسیده‌ایم. خروجی SQL تولید شده توسط EF نیز به صورت زیر است:
SELECT [Project1].[Id] AS [Id],
       [Project1].[Name] AS [Name],
       [Project1].[C1] AS [C1]
FROM   (
           SELECT [Extent1].[Id] AS [Id],
                  [Extent1].[Name] AS [Name],
                  (
                      SELECT COUNT(1) AS [A1]
                      FROM   [dbo].[ProjectIssues] AS [Extent2]
                      WHERE  ([Extent1].[Id] = [Extent2].[ProjectStatusId])
                             AND ([Extent2].[ProjectId] = 1 /*@p__linq__0*/)
                  ) AS [C1]
           FROM   [dbo].[ProjectIssueStatus] AS [Extent1]
       ) AS [Project1]

  • #
    ‫۱۱ سال و ۱۲ ماه قبل، چهارشنبه ۵ مهر ۱۳۹۱، ساعت ۱۰:۵۱
    وای، منظورتون Navigation Property بود؟  نیم ساعته دارم فکر می‌کنم خواص راهبری دیگه چیه؟
  • #
    ‫۱۱ سال و ۱۲ ماه قبل، چهارشنبه ۵ مهر ۱۳۹۱، ساعت ۱۹:۳۸
    در بسیاری از مثالهای این سایت از IList استفاده کردید و در این مثال از ICollection . فرق اینها دقیقا در چیست؟
    • #
      ‫۱۱ سال و ۱۲ ماه قبل، چهارشنبه ۵ مهر ۱۳۹۱، ساعت ۲۱:۱۳
       IEnumerable فقط خواندنی است.
      ICollection یک IEnumerable است که قابلیت Add و Remove به آن اضافه شده.
      IList یک ICollection است که به اعضای آن از طریق ایندکس‌ها می‌توان دسترسی اتفاقی داشت.
      در تمام مثال‌های EF Code first این سایت از
      ICollection برای معرفی خواص راهبری استفاده شده چون EF برای انجام اعمال داخلی خودش به مجموعه‌هایی که قابلیت افزودن یا حذف عناصر را داشته باشند، نیاز دارد. به علاوه اگر به سورس EF هم مراجعه کنید برای تشخیص روابط بین کلاس‌ها به دنبال ICollection می‌گردد.
      همچنین رسم است حین انتخاب اینترفیس‌هایی از این دست که از هم مشتق می‌شوند، روال انتخاب «the least specific abstraction» رعایت شود. یعنی انتخاب کوچکترین پیاده سازی با حداقل نیازهایی که کاربرد مورد نظر را برآورده می‌کند.

      • #
        ‫۱۱ سال و ۱۲ ماه قبل، پنجشنبه ۶ مهر ۱۳۹۱، ساعت ۰۳:۴۴
        مرسی
        خیلی کامل بود.
  • #
    ‫۱۱ سال و ۱۲ ماه قبل، پنجشنبه ۶ مهر ۱۳۹۱، ساعت ۰۳:۵۳
    میتونم بپرسم چرا از toList استفاده کردید؟
    • #
      ‫۱۱ سال و ۱۲ ماه قبل، پنجشنبه ۶ مهر ۱۳۹۱، ساعت ۰۴:۱۵
      این یک عادت خوب در EF است. زمانیکه خروجی کار شما IEnumerable باشد، هر بار دسترسی به نتیجه آن، یکبار رفت و برگشت به بانک اطلاعاتی را سبب خواهد شد. برای نمونه در مثال زیر دوبار رفت و برگشت به بانک اطلاعاتی خواهیم داشت (یکبار در حلقه اول و یکبار در حلقه دوم). اما با استفاده از ToList فقط یکبار رفت و برگشت صورت گرفته و اطلاعات اصطلاحا materialized خواهند شد.
      var list = ctx.ProjectStatus.Select(...);
      
      foreach (var item in list)
      {...}
      
      foreach (var item in list)
      {...}
      • #
        ‫۵ سال و ۱ ماه قبل، جمعه ۱۱ مرداد ۱۳۹۸، ساعت ۲۳:۴۳
        به محض رسیدن به foreach اول اطلاعات از بانک اطلاعاتی واکشی میشوند، طبق آموزش سری ef و قسمت اول اون، قاعدتا نباید برای foreach دوم از کش سطح اول استفاده بکنه و دیگه به بانک رفت و برگشت نزنه ؟ چون شما گفتید که دو بار رفت و برگشت خواهیم داشت. سطح اول کش برای foreach دوم در اینجا کار نخواهد کرد ؟ 
        • #
          ‫۵ سال و ۱ ماه قبل، شنبه ۱۲ مرداد ۱۳۹۸، ساعت ۰۰:۰۳
          - خیر. سطح اول کش در EF 6x فقط به معنای استفاده از متد FindAsync و Find آن است و نه هیچ متد دیگری.
          - فراخوانی متد ToList بر روی یک IEnumerable، اتصال آن‌را از بانک اطلاعاتی قطع می‌کند. به همین جهت اگر list نوشته شده (var list = ctx.ProjectStatus.Select )، یک ToList را در انتها داشت، دو حلقه‌ی بعدی فقط LINQ to Objects می‌شدند (از حافظه خوانده می‌شدند)؛ اما در حالت فعلی آن، هر دو مورد LINQ to Entities هستند و متصل به بانک اطلاعاتی.
          - البته این مورد متصل بودن به بانک اطلاعاتی، می‌تواند مزیت هم باشد. برای مثال ToList، ابتدا تمام رکوردها را به درون حافظه بارگذاری می‌کند؛ اما اگر صرفا با خروجی Select که از نوع IEnumerable هست کار کنید، هربار یک yield return را شاهد خواهید بود که سربار مصرف حافظه‌ی بسیار کمی را دارد و برای کار با تعداد رکورد بالا بسیار مناسب است.
          - برای بررسی دقیق‌تر این موارد همیشه از یک پروفایلر استفاده کنید؛ مانند DNTProfiler و برای مثال بررسی کنید که چه تعدادی کوئری SQL تولید شده. 
  • #
    ‫۱۱ سال و ۱۲ ماه قبل، پنجشنبه ۶ مهر ۱۳۹۱، ساعت ۲۱:۵۳

     شمای edmxی برای درک بهتر.

  • #
    ‫۱۰ سال و ۵ ماه قبل، چهارشنبه ۲۷ فروردین ۱۳۹۳، ساعت ۱۵:۱۴
    من وقتی از IList استفاده میکنم برای تعریف خواص راهبری زمانی که اون رو به یک گرید بایند میکنم AddNewRow گرید کار نمیکنه(devExpress)با جستجو و تعریف bindingList به جای لیست مشکل حل شد.
    میخواستم ببینم تعریف خواص راهبری از نوع bindingList مشکلی ندارد؟
    فقط binginList متد AddRange ندارد و برای اضافه کردن چند لیست به آن باید از foreach استفاده کرد.
    • #
      ‫۱۰ سال و ۵ ماه قبل، چهارشنبه ۲۷ فروردین ۱۳۹۳، ساعت ۱۵:۴۱
      این‌ها بیشتر مباحث binding دو طرفه است. تیم EF هم یک ObservableListSource را برای برنامه‌های WinForm تدارک دیده: اینجا. برای WPF هم اینجا
  • #
    ‫۹ سال و ۱۲ ماه قبل، دوشنبه ۳۱ شهریور ۱۳۹۳، ساعت ۲۳:۱۳
    با سلام
    من یه سوال داشتم، این سوالم مربوط میشه به جدول هایی که توی اینجا  ساختید
    میخواستم بدونم بهینه‌ترین نوع دستور توی EF برای گرفتن خروجی زیر چی میتونه باشه :
    گرفتن FirstName و LastName کاستومر هایی که در نقش با Name ادمین هستند!
    ممنون
  • #
    ‫۹ سال و ۳ ماه قبل، یکشنبه ۱۴ تیر ۱۳۹۴، ساعت ۱۸:۳۵
    اتریبیوت ForeignKey  که در مدل ذکر شده صرفا annotation هست یا وقتی در code first دیتابیس را از طریقUpdate-Database آپدیت می‌کنیم، روی ساختار دیتابیس هم تاثیر می‌گذارد؟
    • #
      ‫۹ سال و ۳ ماه قبل، یکشنبه ۱۴ تیر ۱۳۹۴، ساعت ۱۸:۴۳
      اثر آن‌را بر روی اسکیمای نهایی، در اولین تصویر ارسالی این مطلب می‌توانید مشاهده کنید (_FK‌های تشکیل شده).