Entity Framework应用:导航属性
一、主键和外键
关系型数据库中的一条记录中有若干个属性,若其中某一个属性组是能唯一标识一条记录,该属性组就可以称为主键。例如:
学生版(学号、姓名、性别、班级)
其中每个学生的学号是唯一的,学号就是一个主键。
课程表(课程编号,课程名,学分)
其中课程编号是唯一的,课程编号就是一个主键。
成绩表(学号、课程号、成绩)
成绩表中单独的一个属性无法唯一标识一条记录,学号和课程号的组合才能唯一标识一条记录,所以学号和课程号的属性组是一个主键。
外键
成绩表中的学号不是成绩表的主键,但它和学生表中的学号相对应,并且学生表中的学号是学生表的主键,则称成绩表中的学号是学生表的外键。同理:成绩表中的课程号是课程表的外键。
EntityFramework中的导航属性即外键。下面通过例子讲解如何使用EF的导航属性。
二、导航属性
1、新建产品分类表,语句如下:
1 CREATE table Category 2 ( 3 CategoryId int primary key not null identity, 4 CategoryName varchar(64) 5 )
新建产品明细表,其中CategoryId是外键
1 CREATE TABLE [dbo].[ProductDetail]( 2 [ProductId] [int] IDENTITY(1,1) NOT NULL, 3 [ProductName] [varchar](32) NULL, 4 [Price] [decimal](9, 2) NULL, 5 [CategoryId] [int] NULL, 6 PRIMARY KEY CLUSTERED 7 ( 8 [ProductId] ASC 9 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 10 ) ON [PRIMARY] 11 12 GO 13 14 SET ANSI_PADDING OFF 15 GO 16 17 ALTER TABLE [dbo].[ProductDetail] WITH CHECK ADD CONSTRAINT [FK_Category] FOREIGN KEY([CategoryId]) 18 REFERENCES [dbo].[Category] ([CategoryId]) 19 GO 20 21 ALTER TABLE [dbo].[ProductDetail] CHECK CONSTRAINT [FK_Category] 22 GO
分别往Category表和ProductDetail表中插入一些测试数据:
1 --Category表插入数据 2 INSERT INTO Category (CategoryName) 3 select '电子产品' union 4 SELECT '家用电器' UNION 5 SELECT '图书' 6 7 --ProductDetail表插入数据 8 INSERT INTO ProductDetail (ProductName,Price,CategoryId) 9 SELECT '苹果6s手机',5633,1 UNION 10 SELECT 'Dell电脑',6998,1 UNION 11 SELECT '佳能相机',5633,1 UNION 12 SELECT '海尔洗衣机',1234,2 UNION 13 SELECT '格力空调',2344,2 UNION 14 SELECT '美的冰箱',3218,2 UNION 15 SELECT '白鹿原',342,3 UNION 16 SELECT 'C#高级编程(第十版)',145,3 UNION 17 SELECT '平凡的世界',231,3
2、使用DataBase First模式生成edmx文件,然后查看Category表和ProductDetail表相对应的实体的定义
Category表定义:
1 //------------------------------------------------------------------------------ 2 // <auto-generated> 3 // 此代码已从模板生成。 4 // 5 // 手动更改此文件可能导致应用程序出现意外的行为。 6 // 如果重新生成代码,将覆盖对此文件的手动更改。 7 // </auto-generated> 8 //------------------------------------------------------------------------------ 9 10 namespace EFNavigateDemo 11 { 12 using System; 13 using System.Collections.Generic; 14 15 public partial class Category 16 { 17 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")] 18 public Category() 19 { 20 this.ProductDetails = new HashSet<ProductDetail>(); 21 } 22 23 public int CategoryId { get; set; } 24 public string CategoryName { get; set; } 25 26 [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] 27 public virtual ICollection<ProductDetail> ProductDetails { get; set; } 28 } 29 }
Category实体类中有一个ProductDetail类型的集合属性,表示是导航属性。
实体类型包含其他实体类型(POCO类)的属性(也可称为导航属性),且同时满足如下条件即可实现延迟加载:
1.该属性的类型必须为public且不能为Sealed。
2.属性标记为Virtual。
ProductDetail实体类定义如下:
1 //------------------------------------------------------------------------------ 2 // <auto-generated> 3 // 此代码已从模板生成。 4 // 5 // 手动更改此文件可能导致应用程序出现意外的行为。 6 // 如果重新生成代码,将覆盖对此文件的手动更改。 7 // </auto-generated> 8 //------------------------------------------------------------------------------ 9 10 namespace EFNavigateDemo 11 { 12 using System; 13 using System.Collections.Generic; 14 15 public partial class ProductDetail 16 { 17 public int ProductId { get; set; } 18 public string ProductName { get; set; } 19 public Nullable<decimal> Price { get; set; } 20 public Nullable<int> CategoryId { get; set; } 21 22 public virtual Category Category { get; set; } 23 } 24 }
ProductDetail类里面有一个Category类型的属性。
导航属性实现延迟加载的四种方式:
1、方式一
1 using (var dbContext = new CategoryEntities()) 2 { 3 dbContext.Configuration.LazyLoadingEnabled = true; // 默认是true,针对导航属性 4 var categoryList = dbContext.Set<Category>().Where(p => p.CategoryId == 3); 5 // 只会在数据库里面查询Category表,不会查询ProductDetail表 6 foreach(var category in categoryList) 7 { 8 Console.WriteLine("CategoryId:"+category.CategoryId+ ",CategoryName:"+category.CategoryName); 9 // 这时才会去数据库查询ProductDetail表 10 foreach (var product in category.ProductDetails) 11 { 12 Console.WriteLine("ProductName:"+product.ProductName); 13 } 14 } 15 }
分别在两处foreach循环的地方添加断点,然后运行程序查看数据库执行的SQL语句情况:
执行到断点1时:
这时查看数据库监控:
继续执行到断点2:
这时在查看数据库监控:
会发现遍历ProductDetails属性时也会查询ProductDetail表。
2、方式二
1 using (var dbContext = new CategoryEntities()) 2 { 3 dbContext.Configuration.LazyLoadingEnabled = false; // 不延迟加载,不会再次查询了 4 var categoryList = dbContext.Set<Category>().Where(p => p.CategoryId == 3); 5 // 只会在数据库里面查询Category表,不会查询ProductDetail表 6 foreach (var category in categoryList) 7 { 8 Console.WriteLine("CategoryId:" + category.CategoryId + ",CategoryName:" + category.CategoryName); 9 // 这时不会去数据库查询了,所以用户全是空的 10 foreach (var product in category.ProductDetails) 11 { 12 Console.WriteLine("ProductName:" + product.ProductName); 13 } 14 } 15 }
这时还是采用和上面一样的方法加入断点,只需要查看第二次循环时的数据库监控情况即可:
从上面的截图中看出,如果LazyLoadingEnabled设置为false,将不会再查询ProductDetail表的数据了。
3、方式三
1 // 显示加载 2 using (var dbContext = new CategoryEntities()) 3 { 4 // 不延迟加载,指定Include,一次性加载主表和从表的所有数据 5 var categoryList = dbContext.Set<Category>().Include("ProductDetails").Where(p => p.CategoryId == 3); 6 foreach (var category in categoryList) 7 { 8 Console.WriteLine("CategoryId:" + category.CategoryId + ",CategoryName:" + category.CategoryName); 9 // 不会再查询 10 foreach (var product in category.ProductDetails) 11 { 12 Console.WriteLine("ProductName:" + product.ProductName); 13 } 14 } 15 }
使用Include()方法会一次性加载所有的数据:
4、方式四
在上面的方式2中把LazyLoadingEnabled设置为false以后就不会再查询ProductDetail表的数据了,这时如果想要查询ProductDetail表的数据该怎么办呢?这时可以使用手动加载,代码如下:
1 //LoadProperty 手动加载 2 using (var dbContext = new CategoryEntities()) 3 { 4 dbContext.Configuration.LazyLoadingEnabled = false; // 不延迟加载,不会再次查询了 5 var categoryList = dbContext.Set<Category>().Where(p => p.CategoryId == 3); 6 foreach (var category in categoryList) 7 { 8 Console.WriteLine("CategoryId:" + category.CategoryId + ",CategoryName:" + category.CategoryName); 9 dbContext.Entry<Category>(category).Collection(p => p.ProductDetails).Load();// 集合显示加载 10 foreach (var product in category.ProductDetails) 11 { 12 Console.WriteLine("ProductName:" + product.ProductName); 13 } 14 } 15 }
添加断点:
查看数据库监控:
5、插入数据
对于Category和ProductDetail表如何同时插入数据?先看下面的一段代码:
1 using (var dbContext = new CategoryEntities()) 2 { 3 using (TransactionScope trans = new TransactionScope()) 4 { 5 Category category = new Category() 6 { 7 CategoryName = "自行车" 8 }; 9 dbContext.Categories.Add(category); 10 dbContext.SaveChanges();//category.CategoryId赋值了 11 ProductDetail product = new ProductDetail() 12 { 13 ProductName = "美利达", 14 Price = 2312, 15 CategoryId = category.CategoryId 16 }; 17 18 dbContext.ProductDetails.Add(product); 19 dbContext.SaveChanges(); 20 trans.Complete();//提交事务 21 } 22 }
在第一次SaveChanges()后面的一行代码加断点,查看Category信息:
可以看到这是CategoryId已经有值了,查询数据库ProductDetail表:
这时Product的信息已经插入到数据库中了,而且CategordId也是上面生成的CategoryId。
但是这样会导致一种问题存在:如果第一次SaveChanges()成功,第二次SaveChanges()之前报错了,但是程序已经不能回滚了,这样就会导致数据不一致了。使用下面的代码进行优化:
1 using (var dbContext = new CategoryEntities()) 2 { 3 using (TransactionScope trans = new TransactionScope()) 4 { 5 Category category = new Category() 6 { 7 CategoryName = "汽车" 8 }; 9 10 ProductDetail product = new ProductDetail() 11 { 12 ProductName = "上海大众", 13 Price = 190090, 14 CategoryId = category.CategoryId 15 }; 16 17 category.ProductDetails = new List<ProductDetail>() { product}; 18 dbContext.Categories.Add(category); 19 dbContext.SaveChanges(); 20 trans.Complete();//提交事务 21 } 22 }
经过这样修改以后可以保证数据的一致性了。这是情况只适合有导航属性的。
示例代码下载地址:https://pan.baidu.com/s/1swge4txIlbBuSgs9GspC4g