一、主鍵和外鍵
關系型數據庫中的一條記錄中有若干個屬性,若其中某一個屬性組是能唯一標識一條記錄,該屬性組就可以稱為主鍵。例如:
學生版(學號、姓名、性別、班級)
其中每個學生的學號是唯一的,學號就是一個主鍵。
課程表(課程編號,課程名,學分)
其中課程編號是唯一的,課程編號就是一個主鍵。
成績表(學號、課程號、成績)
成績表中單獨的一個屬性無法唯一標識一條記錄,學號和課程號的組合才能唯一標識一條記錄,所以學號和課程號的屬性組是一個主鍵。
外鍵
成績表中的學號不是成績表的主鍵,但它和學生表中的學號相對應,並且學生表中的學號是學生表的主鍵,則稱成績表中的學號是學生表的外鍵。同理:成績表中的課程號是課程表的外鍵。
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