前言
關於表關系園中文章也是數不勝收,但是個人覺得最難攻克的是一對一,對其配置並非無道理可循,只要掌握了原理方可,且聽我娓娓道來!
共享主鍵關系
概念:就是兩個表共享相同的主鍵值,也就是說一表的主鍵值是另外一個表的外鍵值。
我們現在給出三個類,一個是User(用戶類),一個是Address(地址類),最后一個是Shipment(運貨車類)。每個用戶都對應一個銀行賬戶地址也就是Address,同時運貨車都有一個運貨的地點也就是Address。鑒於此設計類圖如下並且我們建立如下三個類。
/*用戶類*/
public class User { public int UserId { get; set; }
public string Name { get; set; } public virtual Address BillingAddress { get; set; } } /*運貨車類*/ public class Shipment { public int ShipmentId { get; set; }
public string State { get; set; } public virtual Address DeliveryAddress { get; set; } } /*地點類*/ public class Address { public int AddressId { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string ZipCode { get; set; } }
我們通過如下映射來得到一對一的關系:
用戶映射類
public class UserMap:EntityTypeConfiguration<User> { public UserMap() { HasOptional(p => p.BillingAddress).WithRequired(); } }
運貨車映射類
public class ShipmentMap : EntityTypeConfiguration<Shipment> { public ShipmentMap() { HasRequired(p => p.DeliveryAddress).WithOptional(); } }
【注意】在上述關系中我們無需指定外鍵,因為當其屬性暴露在實體中時我們才用HasForeignKey()方法進行指定, 同時因為EF僅僅支持一對一關系在主鍵上,所以它將會自動在數據庫中在主鍵上建立關系。
數據庫設計圖
下面數據庫設計圖是基於EF Code First映射的結果
那么我們如何知道創建的表中誰是主鍵誰是外鍵呢?我們通過參照性完整性規則來看看
參照性完整規則
用戶表外鍵關系
運貨車表外鍵關系
從上述兩個外鍵關系中我們可以看出:EF Code First添加了一個連接地址(Address)的主鍵到用戶(User)主鍵的外鍵約束,同時也添加了一個連接運貨車(Shipment)的主鍵到地址(Address)主鍵的外鍵約束。也就意味着,地址的主鍵依據用戶主鍵來定,而運貨車主鍵根據地址的主鍵來定。
那么問題來了,在關系映射中,EF Code First最終是怎樣決定誰是主體對象誰是依賴對象呢?
不難看出EF Code First是根據你的對象模型來判定的,例如我們上述用一下代碼來判定用戶和地址之間 的關系
HasOptional(p => p.BillingAddress).WithRequired();
這意思就是用戶實體對於地址是可選的關系,但是地址對於用戶卻是必須的關系,所以我們得出結論:在這種關系中,最終用戶將是主體對象而地址最終將是依賴對象。同時通過上述參照完整性規則中的外鍵關系我們也能得出這樣的結論。
不知道你們注意到沒在第一幅圖關於數據庫設計圖中,我還做了標記,生成的UserId是標識列,而AddressId和ShipmentId不是,不信讓你看看它倆Id的標識,如下圖:
依據所給圖我們得出對於一對一關系結論:依賴對象的主鍵默認將不會被標識。
從上我們知道,每一個地址總數屬於一個用戶,每一個運貨車總是對應相應的地址。那么問題又來了,我們是不是只要刪除一個用戶那么是不是地址和運貨車就會相應的進行刪除呢?
默認情況下,EF Code First是不會進行級聯刪除的,我們又要保護參照性完整規則,於是我們只能手動通過Fluent API進行級聯刪除,例如對於用戶來說如下:
HasOptional(p => p.BillingAddress).WithRequired().WillCascadeOnDelete();
其他方法如WithOptionalDependent用來做什么的呢?
HasRequired() 方法返回 RequiredNavigationPropertyConfiguration 對象的類型,在此類中除了我們通常用到的典型的 WithMany() 和WithOptional() 方法外還定義了兩個特別的方法 WithRequiredDependent()和WithRequiredPrincipal() 方法,為什么有這兩個方法呢?我們知道在EF Code Firs指出了在關系中的主體對象和依賴對象的唯一原因是通過Fluent API能夠最終明確指出一個是必須的(Required),另一個是可選的(Optional),但是要是我們在關系中都是必須的或者都是可選的那該怎么辦呢?例如在一種場景下一個地址總是對應一個用戶,一個用戶總是對應一個地址(雙方都是必須的),所以在此種情況下,EF Code First就不能明確指出誰是主體對象誰是依賴對象,於是就引入了WithRequiredDependent()方法,簡而言之,這種配置最終需要Fluent API來完成(不談論Data Annotation),而Fluent API就設計了一種方式,這種方式就是強迫你明確指出誰是主體對象誰是依賴對象在兩個都是可選或者兩個都是必須的條件下。
例如:綜上在User和Address兩個都是必須的前提下,我們如下配置即可:
HasRequired(p => p.BillingAddress).WithRequiredDependent();
請看下圖,正如我們所分析的,當你需要兩者都是必須的時候,是沒有WithRequired()方法在此類中:
接下來我們添加數據進行測試:
EntityDbContext ctx = new EntityDbContext(); Address billingAddress = new Address() { Street = "華容道", City = "岳陽" }; User user = new User() { Name = "莫扎特", BillingAddress = billingAddress }; ctx.Set<User>().Add(user); ctx.SaveChanges();
很顯然我們無需指定UserId,因為上述已經說明其為標識列即自動增長,並且此時AddressId與UserId相同。
接下來我們添加Address數據和Shipment數據
EntityDbContext ctx = new EntityDbContext(); Address deliveryAddress = new Address() { AddressId = 1, Street = "華容道", }; Shipment shipment = new Shipment() { ShipmentId = 1, State = "true", DeliveryAddress = deliveryAddress }; ctx.Set<Shipment>().Add(shipment); ctx.SaveChanges();
此時運行肯定會報錯,因為在第一次添加數據時,AddressId就已經為1,鑒於約束無法為其添加重復值所以無法進行更新!
通過一對於一關系的共享主鍵有一個最大限制:
很難保存相關對象:因為當對象被保存時要確保相關的實例的被分配的主鍵值也相同(例如當新添加一個Address時,你得確保要提供唯一的一個AddressId並且這個AddressId能夠在User中有相同的這樣一個值作為UserId。
概要
通過主鍵共享只是實現一對一關系的一種方式,鑒於上述實現在實際應用中並不常用可以說是相當罕見,在許多場景下,我們更多實現一對一關系是通過添加一個外鍵字段和唯一約束,接下來我們將通過外鍵來實現這種方式將無主鍵共享方式諸多限制。
外鍵關系
我們現在對上面類進行改造,現在場景是每個用戶對應兩個地址,一個是BillingAddress(賬戶地址),一個是FamlilyAddress(家庭地址)!建立類以及類圖如下:
public class User { public int UserId { get; set; } public string Name { get; set; } public int BillingAddressId { get; set; } public int FamlilyAddressId { get; set; } public Address BillingAddress { get; set; } public Address FamlilyAddress { get; set; } } public class Address { public int AddressId { get; set; } public string Street { get; set; } public string City { get; set; }
public string ZipCode {get;set;}
}
此時我們用BillingAddressId和FamlilyAddressId作為BillingAddress和FamliyAddress的導航屬性。
此時我們用這兩個來代表作為導航屬性,但是Fluent API通過約定也並不認識,這是代表外鍵,因此我們需要手動添加外鍵:
public UserMap() { HasRequired(a => a.BillingAddress) .WithMany().HasForeignKey(u => u.BillingAddressId); HasRequired(a => a.DeliveryAddress) .WithMany().HasForeignKey(u => u.FamlilyAddressId); }
創建映射后添加數據進行嘗試是否建立成功:
EntityDbContext ctx = new EntityDbContext(); var user = new User() { UserId = 1, BillingAddress = new Address() { AddressId = 1 }, FamlilyAddress = new Address() { AddressId = 2 } }; ctx.Set<User>().Add(user); ctx.SaveChanges();
一運行居然莫名其妙的出錯了:
基於模型創建數據庫過程中出現錯誤,有個多重級聯路徑。查閱相關資料得到如下結果:
因為在 SQL Server 表不能出現一次以上的所有級聯參照動作由刪除或更新語句啟動列表中,您會收到此錯誤消息。例如,級聯參照動作的樹上級聯引用操作樹必須只能有一個到特定表的路徑。
所以在User表上進行級聯操作的刪除或者更新的話肯定是不止一次,因為Address對應的兩個Id即BillingAddressId和FamlilyAddressId,那進行映射時關掉一個級聯操縱即可。於是乎最終改造如下:
public UserMap() { HasRequired(a => a.BillingAddress) .WithMany().HasForeignKey(u => u.BillingAddressId); HasRequired(a => a.DeliveryAddress) .WithMany().HasForeignKey(u => u.DeliveryAddressId).WillCascadeOnDelete(false); }
通過Sql Profiler監控關鍵的添加外鍵約束語句如下:
ALTER TABLE [dbo].[Users] ADD CONSTRAINT [FK_dbo.Users_dbo.Addresses_BillingAddressId] FOREIGN KEY ([BillingAddressId]) REFERENCES [dbo].[Addresses] ([AddressId]) ON DELETE CASCADE
ALTER TABLE [dbo].[Users] ADD CONSTRAINT [FK_dbo.Users_dbo.Addresses_DeliveryAddressId] FOREIGN KEY ([DeliveryAddressId]) REFERENCES [dbo].[Addresses] ([AddressId])
數據庫關系圖如下:
看上面Fluent API是不是有點驚訝,這和一對多的關系的配置是一樣的。實際上我們把這看做是to-one即其實這種帶外鍵的一對一關系非雙向關系是一種單向關系,通過數據庫關系圖即可得知。那么我們如何使得它變成徹底的一對一的雙向呢?我們上下文可以執行sql命名我們進行手動添加,我們現在試試:
我們在添加數據之前執行一段sql命令
ctx.Database.ExecuteSqlCommand("ALTER TABLE Users ADD CONSTRAINT uc_Billing UNIQUE(BillingAddressId)"); ctx.Database.ExecuteSqlCommand("ALTER TABLE Users ADD CONSTRAINT uc_Delivery UNIQUE(DeliveryAddressId)");
最終我們看重新生成的數據的關系圖如下:
已經是完全的雙向了,至此就完成了通過外鍵導航屬性和唯一約束來實現一對一的關系
總結
有時候實現像上面的一對一實現不了就可以借用手寫sql語句來實現,就像有時候用EF實現比較復雜的業務時也可以考慮用存儲過程來實現。實現是多方式的,最主要還是的看怎樣去實現最合適。當然以上所有列子在實際應用中不會這么奇葩,只是通過這樣的例子來更加深入的學習一對一這樣看似比較簡單但實際上在三種關系中是比較麻煩的一種。