در حین کار با ارتباطات بین اشیاء و جداول، دانستن یک سری از نکات میتوانند در کم کردن تعداد رفت و برگشتهای به سرور مؤثر واقع شده و نهایتا سبب بالا رفتن سرعت برنامه شوند. از این دست میتوان به یک سری نکات ریز همراه با primary-keys و foreign-keys اشاره کرد که در ادامه به آنها پرداخته خواهد شد.
در ابتدا کلاسهای مدل و Context برنامه را به شکل زیر درنظر بگیرید:
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
namespace TestKeys
{
public class Bill
{
public int Id { get; set; }
public decimal Amount { set; get; }
public virtual Account Account { get; set; }
}
public class Account
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Bill> Bills { get; set; }
public DbSet<Account> Accounts { get; set; }
}
public class Configuration : DbMigrationsConfiguration<MyContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
protected override void Seed(MyContext context)
{
var a1 = new Account { Name = "a1" };
var a2 = new Account { Name = "a2" };
var bill1 = new Bill { Amount = 100, Account = a1 };
var bill2 = new Bill { Amount = 200, Account = a2 };
context.Bills.Add(bill1);
context.Bills.Add(bill2);
base.Seed(context);
}
}
public static class Test
{
public static void Start()
{
Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, Configuration>());
using (var ctx = new MyContext())
{
var bill1 = ctx.Bills.Find(1);
Console.WriteLine(bill1.Amount);
}
}
}
}
در اینجا کلاس صورتحساب و حساب مرتبط به آن تعریف شدهاند. سپس به کمک DbContext این دو کلاس در معرض دید EF Code first قرار گرفتهاند و در کلاس Configuration نحوه آغاز بانک اطلاعاتی به همراه تعدادی رکورد اولیه مشخص شده است.
نحوه صحیح مقدار دهی کلید خارجی در EF Code first
تا اینجا یک روال متداول را مشاهده کردیم. اکنون سؤال این است که اگر بخواهیم اولین رکورد صورتحساب ثبت شده توسط متد Seed را ویرایش کرده و مثلا حساب دوم را به آن انتساب دهیم، بهینهترین روش چیست؟ بهینهترین در اینجا منظور روشی است که کمترین تعداد رفت و برگشت به بانک اطلاعاتی را داشته باشد. همچنین فرض کنید در صفحه ویرایش، اطلاعات حسابها در یک Drop down list شامل نام و id آنها نیز وجود دارد.
روش اول:
using (var ctx = new MyContext())
{
var bill1 = ctx.Bills.Find(1);
var a2 = new Account { Id = 2, Name = "a2" };
bill1.Account = a2;
ctx.SaveChanges();
}
این روش مخصوص تازه واردهای EF Code first است و آنطور که مدنظر آنها است کار نمیکند.
به کمک متد Find اولین رکورد یافت شده و سپس بر اساس اطلاعات drop down در دسترس، یک شیء جدید حساب را ایجاد و سپس تغییرات لازم را اعمال میکنیم. در نهایت اطلاعات را هم ذخیره خواهیم کرد.
این روش به ظاهر کار میکنه اما حاصل آن ذخیره رکورد حساب سومی با id=3 در بانک اطلاعاتی است و سپس انتساب آن به اولین صورتحساب ثبت شده.
نتیجه: Id را دستی مقدار دهی نکنید؛ تاثیری ندارد. زیرا اطلاعات شیء جدید حساب، در سیستم tracking مرتبط با Context جاری وجود ندارد. بنابراین EF آنرا به عنوان یک شیء کاملا جدید درنظر خواهد گرفت، صرفنظر از اینکه Id را به چه مقداری تنظیم کردهاید.
روش دوم:
using (var ctx = new MyContext())
{
var bill1 = ctx.Bills.Find(1);
var a2 = ctx.Accounts.Find(2);
bill1.Account = a2;
ctx.SaveChanges();
}
اینبار بر اساس Id دریافت شده از Drop down list، شیء حساب دوم را یافته و به صورتحساب اول انتساب میدهیم. این روش درست کار میکند؛ اما ... بهینه نیست. فرض کنید شیء جاری دارای 5 کلید خارجی است. آیا باید به ازای هر کلید خارجی یکبار از بانک اطلاعاتی کوئری گرفت؟
مگر نه این است که اطلاعات نهایی ذخیره شده در بانک اطلاعاتی متناظر با حساب صورتحساب جاری، فقط یک عدد بیشتر نیست. بنابراین آیا نمیشود ما تنها همین عدد متناظر را بجای دریافت کل شیء به صورتحساب نسبت دهیم؟
پاسخ: بله. میشود! ادامه آن در روش سوم.
روش سوم:
در اینجا بهترین کار و یکی از best practices طراحی مدلهای EF این است که طراحی کلاس صورتحساب را به نحو زیر تغییر دهیم:
public class Bill
{
public int Id { get; set; }
public decimal Amount { set; get; }
[ForeignKey("AccountId")]
public virtual Account Account { get; set; }
public int AccountId { set; get; }
}
به این ترتیب هم navigation property که سبب تعریف رابطه بین دو شیء و همچنین lazy loading اطلاعات آن میشود پابرجا خواهد بود و هم توسط خاصیت جدید AccountId که توسط ویژگی ForeignKey معرفی شده است، ویرایش اطلاعات آن دقیقا همانند کار با یک بانک اطلاعاتی واقعی خواهد شد.
اینبار به کمک خاصیت متناظر با کلید خارجی جدول، مقدار دهی و ویرایش کلیدهای خارجی یک شیء به سادگی زیر خواهد بود؛ خصوصا بدون نیاز به رفت و برگشت اضافی به بانک اطلاعاتی جهت دریافت اطلاعات متناظر با اشیاء تعریف شده به صورت navigation property :
using (var ctx = new MyContext())
{
var bill1 = ctx.Bills.Find(1);
bill1.AccountId = 2;
ctx.SaveChanges();
}
وارد کردن یک شیء به سیستم Tracking
در قسمت قبل عنوان شد که Id را دستی مقدار دهی نکنید، چون تاثیری ندارد. سؤال: آیا میشود این شیء ویژه تعریف شده را به سیستم Tracking وارد کرد؟
پاسخ: بلی. به نحو زیر:
using (var ctx = new MyContext())
{
var a2 = new Account { Id = 2, Name = "a2_a2" };
ctx.Entry(a2).State = System.Data.EntityState.Modified;
ctx.SaveChanges();
}
در اینجا شیء حساب دوم را به صورت دستی و بدون واکشی از بانک اطلاعاتی ایجاد کردهایم. بنابراین از دیدگاه Context جاری هیچ ارتباطی به بانک اطلاعاتی نداشته و یک شیء جدید درنظر گرفته میشود (صرفنظر از Id آن). اما میتوان این وضعیت را تغییر داد. فقط کافی است State آنرا به نحوی که ملاحظه میکنید به Modified تغییر دهیم. اکنون اگر اطلاعات این شیء را ذخیره کنیم، دقیقا حساب با id=2 در بانک اطلاعاتی ویرایش خواهد شد و نه اینکه حساب جدیدی ثبت گردد.