「導航屬性」是實體框架用得算是比較頻繁的概念。sql
首先,它是類型成員,其次,他是屬性,這不是 F 話,而是明確它的本質。那麼,什麼場景下會用到導航屬性呢?重點就落在「導航」一詞上了,當實體 A 須要引用實體 B 時,實體 A 中須要公開一個屬性,經過這個屬性,能找到關聯的實體 B。數據庫
又或者,X 實體表示你的博客,P 實體表示你發的一篇博文。你的博客確定會發不少博文的,因此,X 實體中可能須要一個 List<P> 類型的屬性,這個屬性包含了你的博客所發表的文章。經過一個實體的屬性成員,能夠定位到與之有關聯的實體,這就是導航的用途了。就像你開着車去穿越神農架同樣,迷路了就打開高德導航(前提是不存在定位干擾)。框架
如今跑江湖的人多,經過各類江湖騙術發家致富。有了不正常的財富積累後,他們開始大量買車,還買地打造我的車庫。因而,Person 實體表明這些有錢人,CarData 實體表示他們買的各類壕車。ide
public class Person { public int PID { get; set; } public string Name { get; set; } public int Age { get; set; } public List<CarData> Cars { get; set; } } public class CarData { public Guid CarID { get; set; } public string CarAttribute { get; set; } public decimal Cost { get; set; } }
每一個 Person 都有 Cars 屬性,表示各自所購買的車。這個 Cars 就是導航屬性,經過這個屬性能找到關聯的 CarData 實體。函數
再定義一個數據上下文類。工具
public class MyContext : DbContext { public DbSet<Person> Persons { get { return Set<Person>(); } } }
公開一個 Persons 屬性,便於訪問,固然了,你以爲我那樣寫代碼太多,你能夠直接來這樣。性能
public DbSet<Person> Persons { get; set; }
兩種寫法都是能夠的。測試
這一次,我選擇用 SQLite 數據庫,新的 .net core 框架沒有包含訪問 SQLIte 的程序集,不過不要緊,有 Nuget 啥都能裹進來。怎麼安裝 nuget 包就不用我教了,你會的。最簡單不粗暴的方法就是直接在 nuget 控制檯中執行 install-package 命令。ui
PM> install-package microsoft.entityframeworkcore.sqlite
看到下面這一堆東東就說明完成了。url
回到 MyContext 類,進行一下鏈接字符串的配置。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("data source=TMD.db"); }
重寫 OnConfiguring 方法,再調用 UseSqlite 擴展方法,就能夠設置鏈接字符串了。
還要重寫 OnModelCreating 方法,要作兩件事情:一是爲每一個實體設置主鍵;二是爲兩個實體創建關係。
protected override void OnModelCreating(ModelBuilder modelBuilder) { // 設置主鍵 modelBuilder.Entity<Person>().HasKey(p => p.PID); modelBuilder.Entity<CarData>().HasKey(c => c.CarID); // 映射實體關係,一對多 modelBuilder.Entity<Person>().HasMany(p => p.Cars); }
在本例中,你懂的,一我的能夠有 N 輛車,所以 Person 與 CarData 之間是「一對多」的關,故而實體 Person 能夠 HasMany 個 CarData 對象,其中,Cars 便是導航屬性。
注意:因爲 MyContext 類重寫了 OnConfiguring 方法,因此,在 MyContext 類的構造函數中,無需接收 DbContextOptions<MyContext> 的依賴注入 ,在 Startup.ConfigureServices 方法中也無需再調用 UseSqlite 方法,你只需 Add 一下就能夠了。
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<MyContext>(); services.AddMvc(); }
在 Main 入口點中,先建立 host 實例。
var host = new WebHostBuilder() .UseKestrel() .UseContentRoot(Directory.GetCurrentDirectory()) .UseEnvironment(EnvironmentName.Development) .UseUrls("http://localhost:7552") .UseStartup<Startup>() .Build();
此時,不要急着調用 Run 方法。由於我們還沒建立數據庫呢。固然,你能夠用老週上一篇中介紹的方法,在 nuget 控制檯中,用 Add-Migration 命令添加遷移,而後用 Update-Database 命令建立數據庫。不過,本文中,老周將經過代碼在運行階段建立數據庫。
using (IServiceScope scope = host.Services.CreateScope()) { MyContext cxt = scope.ServiceProvider.GetRequiredService<MyContext>(); if (cxt.Database.EnsureCreated()) { // 插入一些記錄 Person p1 = new Person { Name = "王老三", Age = 65, Cars = new List<CarData> { new CarData { CarAttribute= "黃色蘭博基尼", Cost = 1500020002.00M }, new CarData { CarAttribute = "景泰藍 吉利瑞博GE", Cost = 138_000M } } }; cxt.Persons.Add(p1); Person p2 = new Person { Name = "朱大日", Age = 72, Cars = new List<CarData> { new CarData { CarAttribute = "瑪瑙紅 別克VELITE 5", Cost = 289_500M }, new CarData { CarAttribute = "雅韻金 本田INSPIRE", Cost = 171000M }, new CarData { CarAttribute = "奧迪A4L", Cost = 401000M } } }; cxt.Persons.Add(p2); // 更新到數據庫 cxt.SaveChanges(); } }
IServiceScope 是個有趣的玩意兒,它建立一個基於當前做用域的服務列表,從該對象中獲取的服務實例,其生命週期只在當前 scope 中有效。這特別適用於臨時實例化服務的情景。好比這裏,MyContext 只是暫時實例化,等建立數據庫並寫入測試數據後,就能夠 Dispose 了。
初始化數據庫後,能夠運行 host 了。
host.Run();
添加一個控制器,爲了簡單,我們不建立 View 了,就直接返回 JSON 數據好了,就當 Web API 來使用。
[Route("[controller]/[action]")] public class TestController : Controller { readonly MyContext context; public TestController(MyContext c) { context = c; } [HttpGet] public ActionResult ListData() { return Json(context.Persons); } }
如今能夠運行了,用諸如 Postman 等測試工具,請求 <root url>/test/listdata,結果發現驚人一幕。
[ { "pid": 1, "name": "王老三", "age": 65, "cars": null }, { "pid": 2, "name": "朱大日", "age": 72, "cars": null } ]
我相信,不少人都遇到了這個問題,因此,本文老周也順便解釋一下這個問題。如你所見,cars 屬性是 null,明明是添加了 CarData 對象的,爲啥會 null,你是否是開始懷疑人生了?千萬不要輕易懷疑人生,那樣是很不負責任的。
好,不賣關子了。出現這個問題,是由於導航屬性的狀態在默認狀況下不會自動去還原的,否則的話,會增長對象引用,因此默認是不加載的。那麼,你會問,那麼 CarData 實體的數據記錄到底加載了沒?加載了的,你能夠寫一個 Action 去試試的。
[HttpGet] public ActionResult CarList() { var cars = context.Set<CarData>().ToList(); return Json(cars); }
而後,你訪問一下 <root url>/test/carlist,看看下面的結果。
[ { "carID": "36e97ed0-56b1-4d92-bb2d-aeec9f9e1b43", "carAttribute": "黃色蘭博基尼", "cost": 1500020002 }, { "carID": "0fd6c2a0-d4ef-4838-bc08-43a5cb024eef", "carAttribute": "景泰藍 吉利瑞博GE", "cost": 138000 }, { "carID": "c9eb20c8-931e-4563-b380-cbee926015c8", "carAttribute": "瑪瑙紅 別克VELITE 5", "cost": 289500 }, { "carID": "3d563693-5ae0-4682-bd53-c7fc87e951de", "carAttribute": "雅韻金 本田INSPIRE", "cost": 171000 }, { "carID": "2294a556-fd02-49c3-b4b2-559f15413e75", "carAttribute": "奧迪A4L", "cost": 401000 } ]
我沒騙你吧,有數據的呀。
如今咱們有這個需求,要求還原導航屬性的狀態,那咋辦呢?再次回到 ListData 方法,把它改爲這樣。
[HttpGet] public ActionResult ListData() { var persons = context.Persons.Include(p => p.Cars).ToList(); return Json(persons); }
調用 Include 方法記得引入 Microsoft.EntityFrameworkCore 命名空間,這個不用我多說了。Incluse 擴展方法的意思就是加載導航屬性中的內容,它會自動還原狀態,知道哪些 CarData 實例與 Person 實例有關。
再次運行,請求一下 <root url>/test/listdata,這下你就放心了。
[ { "pid": 1, "name": "王老三", "age": 65, "cars": [ { "carID": "36e97ed0-56b1-4d92-bb2d-aeec9f9e1b43", "carAttribute": "黃色蘭博基尼", "cost": 1500020002 }, { "carID": "0fd6c2a0-d4ef-4838-bc08-43a5cb024eef", "carAttribute": "景泰藍 吉利瑞博GE", "cost": 138000 } ] }, { "pid": 2, "name": "朱大日", "age": 72, "cars": [ { "carID": "c9eb20c8-931e-4563-b380-cbee926015c8", "carAttribute": "瑪瑙紅 別克VELITE 5", "cost": 289500 }, { "carID": "3d563693-5ae0-4682-bd53-c7fc87e951de", "carAttribute": "雅韻金 本田INSPIRE", "cost": 171000 }, { "carID": "2294a556-fd02-49c3-b4b2-559f15413e75", "carAttribute": "奧迪A4L", "cost": 401000 } ] } ]
怎麼樣,滿意了吧。
接下來,我們再看看反過來的狀況,咋返過來呢?咱們假設以汽車爲主,如今是每輛車都對應着一位車主信息,每一個人只與一輛車關聯,因此,車與人之間是「一對一」的關係。
先定義實體類,結構與前面的差很少。
public class Person { public int PID { get; set; } public string Name { get; set; } } public class CarData { public int CarID { get; set; } public string CarAttribute { get; set; } public decimal Cost { get; set; } public Person Owner { get; set; } }
這一次,如你所見,導航屬性是 CarData 類的 Owner 屬性,即該車的車主信息,引用一個 Person 實例。
下面定義 DbContext。
public class MyContext : DbContext { public DbSet<Person> Persons { get; set; } public DbSet<CarData> Cars { get; set; } }
重寫 OnModelCreating 方法。
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Person>().HasKey(p => p.PID); modelBuilder.Entity<CarData>().HasKey(c => c.CarID); modelBuilder.Entity<CarData>().HasOne(c => c.Owner); }
除了每兩個實體設置主鍵外,請注意看最後一行,這一次,CarData 實體只對應着一個 Person 實例,因此是 HasOne,導航屬性是 Owner。
重寫 OnConfiguring 方法,配置鏈接字符串,這一次就用 SQL Server LocalDB,輕量級的。
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer("server=(localdb)\\MSSQLLocalDB;database=DemoDt"); }
Main 方法中的作法與前面同樣,初始化 WebHost 後,先建立數據庫,填一下垃圾數據,而後再啓動 host。
var host = new WebHostBuilder() .UseKestrel() .UseEnvironment("debug") .UseUrls("http://localhost:19230") .UseStartup<Startup>() .UseContentRoot(Directory.GetCurrentDirectory()) .Build(); using(IServiceScope scope = host.Services.CreateScope()) { MyContext cxt = scope.ServiceProvider.GetRequiredService<MyContext>(); if (cxt.Database.EnsureCreated()) { Person p1 = new Person { Name = "王阿基" }; Person p2 = new Person { Name = "劉二打" }; Person p3 = new Person { Name = "李無牙" }; CarData c1 = new CarData { CarAttribute = "三無產品 A款", Cost = 150000M, Owner = p1 }; CarData c2 = new CarData { CarAttribute = "三無產品 F款", Cost = 67500M, Owner = p2 }; CarData c3 = new CarData { CarAttribute = "三無產品 2018款", Cost = 76000M, Owner = p3 }; cxt.Persons.Add(p1); cxt.Persons.Add(p2); cxt.Persons.Add(p3); cxt.Cars.Add(c1); cxt.Cars.Add(c2); cxt.Cars.Add(c3); cxt.SaveChanges(); } } host.Run();
接下來,建立一個控制器。
public class SampleController : Controller { }
經過依賴注入,得到 MyContext 實例。
readonly MyContext context; public SampleController(MyContext cxt) { context = cxt; }
定義一個獲取數據列表的 Action。
[HttpGet] public ActionResult List() { var cars = context.Cars.Include(c => c.Owner); return Json(cars.ToList()); }
Include 方法的調用與前面同樣,只是注意此次是以 CarData 實體爲主,順便加載導航屬性 Owner 的內容。
Postman 測試結果。
[ { "carID": 1, "carAttribute": "三無產品 A款", "cost": 150000, "owner": { "pid": 1, "name": "王阿基" } }, { "carID": 2, "carAttribute": "三無產品 F款", "cost": 67500, "owner": { "pid": 2, "name": "劉二打" } }, { "carID": 3, "carAttribute": "三無產品 2018款", "cost": 76000, "owner": { "pid": 3, "name": "李無牙" } } ]
一樣,這也達到預期的效果了。
咱們查看一下所生成的數據庫,你會發現,Cars 表中生成了一列,名爲 OwnerPID,引用的是關聯的 Person 實例的主鍵。加載數據時,就是經過這一列來還原兩個實體之間的關係的。