EntityFramework之一对一关系(二)
前言
关于表关系园中文章也是数不胜收,但是个人觉得最难攻克的是一对一,对其配置并非无道理可循,只要掌握了原理方可,且听我娓娓道来!
共享主键关系
概念:就是两个表共享相同的主键值,也就是说一表的主键值是另外一个表的外键值。
我们现在给出三个类,一个是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实现比较复杂的业务时也可以考虑用存储过程来实现。实现是多方式的,最主要还是的看怎样去实现最合适。当然以上所有列子在实际应用中不会这么奇葩,只是通过这样的例子来更加深入的学习一对一这样看似比较简单但实际上在三种关系中是比较麻烦的一种。