“导航属性”是实体框架用得算是比较频繁的概念。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 实例的主键。加载数据时,就是经过这一列来还原两个实体之间的关系的。