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对应 ...
随机推荐
- C# 完整List例子
C# List Following examples show how to create and manipulate with .NET strongly typed list List<T ...
- _MSC_VER详细介绍
_MSC_VER详细介绍 转自:http://www.cnblogs.com/braver/articles/2064817.html _MSC_VER是微软的预编译控制. _MSC_VER可以分解为 ...
- Java的四种引用方式
一.引用基本概念 从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期.这四种级别由高到低依次为:强引用.软引用.弱引用.虚引用. 1.强引用(StrongRef ...
- python 函数之day3
一 函数的语法及特性 什么是函数? 定义:函数是一个功能通过一组语句的集合,由名字(函数名)将其封装起来的代码块,要想执行这个函数,只要调用其函数名即可. 特性: 减少重复代码 使程序变的可扩展 使程 ...
- web程序的路径笔记
"/"与”\“区别:”/“是unix系统区分文件层级的标志,因为当前web应用程序在服务器端大都使用基于unix系统开发的操作系统,所以web程序包括浏览器里url都遵以”/“来区 ...
- Usaco*Brownie Slicing
Description Bessie烘焙了一块巧克力蛋糕.这块蛋糕是由R*C(1 <= R,C <= 500)个小的巧克力蛋糕组成的. 第i行,第j列的蛋糕有N_ij(1 <= N_ ...
- ant的安装及项目的发布
1.安装ant1) 直接解压apache-ant-1.9.7-bin 2) 在环境变量中配置,ant_home的环境变量在 3) 在命令提示符中测试是否安装成功. 2 项目首次打包1) 写好打包的配置 ...
- HTML实践发现(标签<pre>)
1. (1).第一种编辑: (2).浏览器中显示: 2. (1).第二种编辑 (2).浏览器中显示: 结果发现:使用标签<pre>,在浏览器中显示的结果与在<pre>下方编写的 ...
- 安卓 自定义AlertDialog对话框(加载提示框)
AlertDialog有以下六种使用方法: 一.简单的AlertDialog(只显示一段简单的信息) 二.带按钮的AlertDialog(显示提示信息,让用户操作) 三.类似ListView的Aler ...
- 《R in Action》读书笔记(2)
MindMapper 原文件