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 = ,
Street = "华容道",
}; Shipment shipment = new Shipment()
{
ShipmentId = ,
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 = ,
BillingAddress = new Address() { AddressId = },
FamlilyAddress = new Address() { AddressId = }
};
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实现比较复杂的业务时也可以考虑用存储过程来实现。实现是多方式的,最主要还是的看怎样去实现最合适。当然以上所有列子在实际应用中不会这么奇葩,只是通过这样的例子来更加深入的学习一对一这样看似比较简单但实际上在三种关系中是比较麻烦的一种。
EntityFramework之一对一关系(二)的更多相关文章
- EntityFramework 建立一对一关系
前言:本来要使用实体拆分实现一对一,但发现查询时无法单独查询,影响效率,故改用手动建立一对一关系 例: 实体类: public class TestDbContext : DbContext { pu ...
- 问题记录:EntityFramework 一对一关系映射
EntityFramework 一对一关系映射有很多种,比如主键作为关联,配置比较简单,示例代码: public class Teacher { public int Id { get; set; } ...
- hibernate(五) hibernate一对一关系映射详解
序言 之前讲解了一对多(单向.双向).多对多(双向),今天就讲解一下最后一个关系,一对一. 心情不错.状态也挺好的,赶紧写一篇博文造福一下大家把. --WH 一.一对一关系的概述 一对一关系看起来简单 ...
- Entity Framework - 基于外键关联的单向一对一关系
代码的世界,原以为世界关系很简单,确道是关系无处不在.NET世界里ORM框架中EntityFramework作为其中翘楚,大大解放了搬砖工作的重复工作,着实提高了不少生产力,而也碰到过不少问题!比如关 ...
- Hibernate学习(五)———— hibernate一对一关系映射详解
一.一对一关系的概述 一对一关系看起来简单,其实也挺复杂的.其中关系就包含了四种,单向双向和主键关联外键关联. 什么意思呢,也就是包含了单向一对一主键关联.双向一对一主键关联,单向一对一外键关联,双向 ...
- Entity Framework管理实体关系(一):管理一对一关系
我们现在已经知道如何使用Code First来定义简单的领域类,并且如何使用DbContext类来执行数据库操作.现在我们来看下数据库理论中的多样性关系,我们会使用Code First来实现下面的几种 ...
- Hibernate One-to-One Mappings 一对一关系映射
Hibernate One-to-One Mappings 一对一关系映射 关键:一对一关系映射和多对一关系映射非常像.仅仅是unique 属性值为 true 样例:一个员工仅仅能有一个地址. Hib ...
- 数据库表设计时一对一关系存在的必要性 数据库一对一、一对多、多对多设计 面试逻辑题3.31 sql server 查询某个表被哪些存储过程调用 DataTable根据字段去重 .Net Core Cors中间件解析 分析MySQL中哪些情况下数据库索引会失效
数据库表设计时一对一关系存在的必要性 2017年07月24日 10:01:07 阅读数:694 在表设计过程中,我无意中觉得一对一关系觉得好没道理,直接放到一张表中不就可以了吗?真是说,网上信息什么都 ...
- (五)mybatis之一对一关系
一.需求分析 需求:查询订单信息关联查询用户信息 分析:一条订单只能由一个消费者来下单,也就是说从订单的角度来说与消费者是一对一的关系. 二.建数据库表和实体对象 其中订单表中的字段user_id对应 ...
随机推荐
- Bootstrap 简洁、直观、强悍的前端开发框架,让web开发更迅速、简单。
Bootstrap 简洁.直观.强悍的前端开发框架,让web开发更迅速.简单.
- Salesforce入门学习介绍
大家好,本人作为重庆德勤2016年的实习生,进公司实习后有幸接触到了Salesforce,通过一个多月的自学以及培训,准备和大家分享一下我的Salesforce学习之路. 一.什么是Salesforc ...
- sql查询重复数据
select *from Awhere id in (select id from A group by id having count(1) >= 2) 注释:id 为重复的关键字(更换成所需 ...
- 在发送ajax请求时加时间戳或者随机数去除js缓存
在发送ajax请求的时候,为了保证每次的都与服务器交互,就要传递一个参数每次都不一样,这里就用了时间戳 大家在系统开发中都可能会在js中用到ajax或者dwr,因为IE的缓存,使得我们在填入相同的值的 ...
- yii笔一----基础,安装,结构,增删改查基本操作
从yii中文站开始http://www.yiichina.com/ Yii 是一个高性能,基于组件的 PHP 框架 一. 1.安装yii方式 composer安装或者下载一份应用程序模板.刚开始学习, ...
- vector删除元素浅析
<<effectSTL>>书中提到erase-remove方法 即c.rease(remove(c.begin(),c.end(),1963),c.end()) 关于remo ...
- NPOI、MyXls、Aspose.Cells 导入导出Excel(转)
Excel导入及导出问题产生: 从接触.net到现在一直在维护一个DataTable导s出到Excel的类,时不时还会维护一个导入类.以下是时不时就会出现的问题: 导出问题: 如果是asp.net,你 ...
- java学习之面向对象(4)
之前介绍了java面向对象三大特性之一封装,现在来说说三大特性之一继承和抽象类.这些只是我个人的认识,不足之处还请见谅. 1. 继承是面向对象的三大特征之一,那么何为继承呢? 继承是指一个对象直接使用 ...
- C#_技巧:计算代码块运行的时间
System.Diagnostics下类Stopwatch,给程序代码块运行计时, 利用start()和stop()方法来标记代码快. 该命名空间下还有一些其他类,可以对程序进行诊断(diagnosi ...
- 梦想还需有,因它必实现——发现最新版iOS漏洞,OverSky团队专访
梦想还需有,因它必实现——发现最新版iOS漏洞,OverSky团队专访 “成功了!”,随着一句欢呼声在阿里巴巴西溪园区传出,Cydia的图标出现在一部iOS9.3.4的iPhone6上并成功运行 ...