EF Code First 一对多、多对多关联,如何加载子集合?
应用场景
先简单描述一下标题的意思:使用 EF Code First 映射配置 Entity 之间的关系,可能是一对多关系,也可能是多对多关系,那如何加载 Entity 下关联的 ICollection 集合对象呢?
上面的这个问题,我觉得大家应该都遇到过,当然前提是使用 EF Code First,有人会说,在 ICollection 集合对象前加 virtual 导航属性,比如:
public virtual ICollection<Role> Roles { get; set; }
然后在 DbContext 初始化的时候,增加懒加载(或延迟加载)配置:
public UserDbContext()
: base("name=UserDbContext")
{
this.Configuration.LazyLoadingEnabled = false;
}
这种方式当然可以,也是我们常用的一种方式,但这种方式在一种场景中无法使用,就是对关联 ICollection 集合增加 Where 条件,什么意思呢?我下描述一下用户-角色应用场景,一个用户有多个权限,一个权限也可能对应多个用户,所以用户和角色之间的关系是多对多,我们用 EF Code First 进行实现一下:
User(用户)和 Role(角色)实体类:
namespace UserRoleDemo.Entities
{
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Age { get; set; }
public string Address { get; set; }
public DateTime DateAdded { get; set; }
public virtual ICollection<Role> Roles { get; set; }
}
public class Role
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime DateAdded { get; set; }
public virtual ICollection<User> Users { get; set; }
}
}
UserRoleDbContext 映射配置:
public class UserRoleDbContext : DbContext
{
public UserRoleDbContext()
: base("name=UserRoleDb")
{
//this.Configuration.LazyLoadingEnabled = false;
}
public virtual DbSet<User> Users { get; set; }
public virtual DbSet<Role> Role { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder
.Configurations
.Add(new UserConfiguration())
.Add(new RoleConfiguration());
base.OnModelCreating(modelBuilder);
}
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
HasKey(c => c.Id);
Property(c => c.Id)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
HasMany(t => t.Roles)
.WithMany(t => t.Users)
.Map(m =>
{
m.ToTable("UserRole");
m.MapLeftKey("UserId");
m.MapRightKey("RoleId");
});
}
}
public class RoleConfiguration : EntityTypeConfiguration<Role>
{
public RoleConfiguration()
{
HasKey(c => c.Id);
Property(c => c.Id)
.IsRequired()
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
}
}
}
生成对应数据库:
可以看到,我们项目中只有 User 和 Role 两个实体对象,但是生成数据库多了一个 UserRole 表,这个是我们在 UserConfiguration 进行映射配置的结果,当然你不配置也可以,EF Code First 会自动帮你映射,但映射关联表的名字和字段就不能自定义了,如果你深入使用 EF Code First 你会越发觉得它的强大之处,因为它会让你感受不到数据库的“存在”,在应用程序中,所有都是对象之间的操作,没有了事务脚本模式的代码,你可以专注于应用对象的“研究”,即使再复杂的映射配置,EF Code First 也会帮你完成。比如这样一段代码:user.Roles,如果常规的方式(SQL),你会去在应用程序中编写“User join UserRole”的 SQL 代码,但是如果使用 EF Code First,只要映射配置正确,直接 user.Roles 就可以了,当然它不仅如此。
咳咳,扯的有点远了,有点像为微软打广告的意思,呵呵。
言归正传,用户角色的场景就这么简单,上面我说过不能使用懒加载方式解决的问题,比如我要获取一个 User 对象,但在访问 user.Roles 集合的时候,Roles 集合中 Role 对象的 DateAdded 必须大于昨天。这个就不能使用懒加载方式了,因为必须要在 user.Roles 去编写 Where 条件,而懒加载方式是获取所有关联对象的集合,怎么解决这个实际问题呢?请看下面。
问题分析
查询场景:获取 Id 为 1 的 User 对象,并且 User 下的 Roles 集合的 DateAdded 大于昨天。
问题很简单,就是这段话怎么翻译成代码?或者怎么用 Linq 的方式写出来?
有人可能会想到 Include,但使用这种方式就没必要 user.Roles 了,这种方式不可取,然后我再网上找了另一种方式,使用 Any 或 All,大致代码如下:
using (var context = new UserRoleDbContext())
{
var user = context.Users
.Where(u => u.Id == 1)
.Where(u => u.Roles.All(r => r.DateAdded > DateTime.Now.AddDays(-1)))
.FirstOrDefault();
foreach (var role in user.Roles)
{
Console.WriteLine(role.DateAdded);
}
}
使用 Sql Server Profiler 跟踪生成的 SQL 代码,就会发现,我们写的 DateAdded > DateTime.Now.AddDays(-1) 条件会出现在 User 获取中,下面 user.Roles 遍历的时候,还是会加载关联下的所有集合对象,当然这种方式使用必须要开启懒加载。
我个人觉得,这个问题应该在很多应用场景中都会出现,但遗憾的是网上实在找不到响应的解决方案(映射配置的比较多,但获取方式的基本上没有),当然不是说没有方式解决,最简单的就是把集合全部加载出来,然后在内存中进行过滤,项目简单的还好,如果数据量非常大,这种方式也是不可取的,最后在 MSDN 上找到一篇很多年的博客:Using DbContext in EF 4.1 Part 6: Loading Related Entities,注意 EF 版本是 4.1,现在 7.0 都快出来了,哎!
看到“Loading Related Entities”这个标题,我就知道这篇博客就是我想要的,然后按照它描述的,配置如下:
首先,禁止懒加载:
this.Configuration.LazyLoadingEnabled = false;
Linq 查询代码:
using (var context = new UserRoleDbContext())
{
var user = context.Users
.Where(u => u.Id == 1)
.FirstOrDefault();
context.Entry(user)
.Collection(u => u.Roles)
.Query()
.Where(r => r.DateAdded > DateTime.Now.AddDays(-1))
.Load();
foreach (var role in user.Roles)
{
Console.WriteLine(role.DateAdded);
}
}
先说明一下,这段代码是不能运行的,因为 user.Roles 集合的值为 null,至于原因,我是后来才知道的,这种方式只适用于“一对多”的关系,哪篇博客中的演示场景也是“一对多”,如果我们把 Query() 和后面的 Where 代码去掉,没有了条件查询,这段代码时可以运行的,至于原因,我觉得没有了 where,那和懒加载又有什么区别呢。
“一对多”的方式是这种,那“多对多”的呢?答案是在 Collection 后加 Include,示例代码:
using (var context = new UserRoleDbContext())
{
var user = context.Users
.Where(u => u.Id == 1)
.FirstOrDefault();
context.Entry(user)
.Collection(u => u.Roles)
.Query()
.Include(r => r.Users)
.Where(r => r.DateAdded > DateTime.Now.AddDays(-1))
.Load();
foreach (var role in user.Roles)
{
Console.WriteLine(role.DateAdded);
}
}
这种方式确实是可以运行成功的,也是我们想要的效果,但如果你看一下跟踪生成的 SQL 代码,你就不想使用它了,为什么?我们看一下生成的 SQL 代码:
SELECT
[Project1].[UserId] AS [UserId],
[Project1].[RoleId] AS [RoleId],
[Project1].[Id] AS [Id],
[Project1].[Name] AS [Name],
[Project1].[DateAdded] AS [DateAdded],
[Project1].[C1] AS [C1],
[Project1].[Id1] AS [Id1],
[Project1].[Name1] AS [Name1],
[Project1].[Age] AS [Age],
[Project1].[Address] AS [Address],
[Project1].[DateAdded1] AS [DateAdded1]
FROM ( SELECT
[Extent1].[UserId] AS [UserId],
[Extent1].[RoleId] AS [RoleId],
[Extent2].[Id] AS [Id],
[Extent2].[Name] AS [Name],
[Extent2].[DateAdded] AS [DateAdded],
[Join2].[Id] AS [Id1],
[Join2].[Name] AS [Name1],
[Join2].[Age] AS [Age],
[Join2].[Address] AS [Address],
[Join2].[DateAdded] AS [DateAdded1],
CASE WHEN ([Join2].[UserId] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1]
FROM [dbo].[UserRole] AS [Extent1]
INNER JOIN [dbo].[Roles] AS [Extent2] ON [Extent1].[RoleId] = [Extent2].[Id]
LEFT OUTER JOIN (SELECT [Extent3].[UserId] AS [UserId], [Extent3].[RoleId] AS [RoleId], [Extent4].[Id] AS [Id], [Extent4].[Name] AS [Name], [Extent4].[Age] AS [Age], [Extent4].[Address] AS [Address], [Extent4].[DateAdded] AS [DateAdded]
FROM [dbo].[UserRole] AS [Extent3]
INNER JOIN [dbo].[Users] AS [Extent4] ON [Extent4].[Id] = [Extent3].[UserId] ) AS [Join2] ON [Extent2].[Id] = [Join2].[RoleId]
WHERE ([Extent1].[UserId] = @EntityKeyValue1) AND ([Extent2].[DateAdded] > (SysDateTime()))
) AS [Project1]
ORDER BY [Project1].[UserId] ASC, [Project1].[RoleId] ASC, [Project1].[Id] ASC, [Project1].[C1] ASC
看见这一坨的代码就心烦,而且这只是两段 SQL 代码的一个,因为上面我们使用:context.Users.FirstOrDefault(),也会生成一坨 SQL 代码,只不过没那么复杂而已,其实复杂之处,就是我们使用 Include 方式,把 User、Role 和 UserRole 表关联起来使用了,其实我们只是想获取某个 user 下的 Role 集合而已,在 stackoverflow 中有人也有同样的问题:EF 4.1 loading filtered child collections not working for many-to-many,当然讲的比我详细多了。
其实最后的解决方式有点“无语”,为什么呢?看一下代码就知道了:
using (var context = new UserRoleDbContext())
{
var user = context.Users
.Where(u => u.Id == 1)
.FirstOrDefault();
user.Roles = context.Entry(user)
.Collection(u => u.Roles)
.Query()
.Where(r => r.DateAdded > DateTime.Now)
.ToList();
foreach (var role in user.Roles)
{
Console.WriteLine(role.DateAdded);
}
}
你可能发现了与上面代码的不同,就是我们使用 Entry 获取集合对象,重新给 user.Roles 属性赋值,因为 ToList 了,同样会产生两条 SQL 代码,但这种代码,我们是可以接受的:
SELECT
[Extent2].[Id] AS [Id],
[Extent2].[Name] AS [Name],
[Extent2].[DateAdded] AS [DateAdded]
FROM [dbo].[UserRole] AS [Extent1]
INNER JOIN [dbo].[Roles] AS [Extent2] ON [Extent1].[RoleId] = [Extent2].[Id]
WHERE ([Extent1].[UserId] = @EntityKeyValue1) AND ([Extent2].[DateAdded] > (SysDateTime()))
示例 Demo 下载:
非常珍贵的参考资料:
- Using DbContext in EF 4.1 Part 6: Loading Related Entities
- EF 4.1 loading filtered child collections not working for many-to-many
EF Code First 一对多、多对多关联,如何加载子集合?的更多相关文章
- 多对多关联懒加载导致failed to lazily initialize a collection of role: 实体类, could not initialize proxy - no Session 追加配置fetch = FetchType.EAGER解决
一篇文章需要关联很多个标签,所以他们呈一对多(多对多)的关系 org.springframework.web.util.NestedServletException: Request processi ...
- Entity Framework Code First实体关联数据加载
在项目过程中,两个实体数据之间在往往并非完全独立的,而是存在一定的关联关系,如一对一.一对多及多对多等关联.存在关联关系的实体,经常根据一个实体的实例来查询获取与之关联的另外实体的实例. Entity ...
- 第五节: EF高级属性(一) 之 本地缓存、立即加载、延迟加载(不含导航属性)
一. 本地缓存 从这个章节开始,介绍一下EF的一些高级特性,这里介绍的首先介绍的EF的本地缓存,在前面的“EF增删改”章节中介绍过该特性(SaveChanges一次性会作用于本地缓存中所有的状态的变化 ...
- MyBatis 一对多,多对一关联查询的时候Mapper的顺序
要先写association,然后写collection:这是由DTD决定的: <resultMap ...> <association ...> </associati ...
- EF连接Sqlserver2014,使用DBGeography时提示无法加载sqlserverspatial.dll
(1)确认你要使用的SqlServer版本,如果是2014,就要在nuget中添加microsoft.sqlserver.types.dll,使用12.0.4100.1这个版本,它会自动添加sqlse ...
- 多表关联懒加载导致的org.hibernate.LazyInitializationException: could not initialize proxy - no Session
本来考虑的是懒加载,这样可以提高效率,不过由于时间紧急 把懒加载改为急加载临时解决 https://www.jianshu.com/p/89520964f458 自己管理session也可以 临时补丁 ...
- Entity Framework关联实体的三种加载方法
推荐文章 EF性能之关联加载 总结很好 一:介绍三种加载方式 Entity Framework作为一个优秀的ORM框架,它使得操作数据库就像操作内存中的数据一样,但是这种抽象是有性能代价的,故鱼和熊掌 ...
- EF Code First一对一、一对多、多对多关联关系配置
1.EF Code First一对一关联关系 项目结构图: 实体类: Account.cs using System; using System.Collections.Generic; using ...
- mybatis的执行流程 #{}和${} Mysql自增主键返回 resultMap 一对多 多对一配置
n Mybatis配置 全局配置文件SqlMapConfig.xml,配置了Mybatis的运行环境等信息. Mapper.xml文件即Sql映射文件,文件中配置了操作数据库的Sql语句.此文件需要在 ...
随机推荐
- SweetAlert-js超酷消息警告框插件
简要教程 SweetAlert是一款神奇的javascript弹出消息警告框插件. 来通过一张gif图片看看SweetAlert的效果: 使用方法 要使用该插件,首先要在html的header中引入以 ...
- 【Beta】Daily Scrum Meeting第二次
1.任务进度 学号 已完成 接下去要做 502 系负责人及所负责专业的表 写出PHP测试的demo:将okHttp的请求放在非UI线程中执行 509 PHP更该用户信息:更新系负责人所负责系:删除任务 ...
- jQuery图片滚动插件
//该组件目前仅适用于一次移动一张图片的情况 (function ($) { $.fn.extend({ "scroll": function (options) { option ...
- 解决Unity5+Vuforia+Network本地联机发布到Android上白屏的问题
Unity5+Vuforia+Network本地联机,在Android下点击联机,然后识别模型就出现白屏,点击屏幕上相应位置的按钮(已白屏,但点击该看不见的按钮)还是能起作用,如跳转到其他场景正常. ...
- 在手机网页上模拟 js 控制台
在手机上模拟 console 做一些简单代码调试 在工作机上编辑好代码用QQ 之类的工具传到 手机上在调试当然你也可以尝试用一只手指写代码的壮举设置 window.console = mobiDeb ...
- WebServer+ADO+百万数据查询
很简单的demo,查询速度快,易理解,废话不说 上demo 看完就明白了 源码地址:http://files.cnblogs.com/files/SpadeA/WebDemo.zip 这是关于Web ...
- 资深人士剖析微软开源.NET事件:战略重心已经从PC转移到云端
本文是雷锋网对我的访谈整理的文章,源地址是 http://www.leiphone.com/news/201411/6KaGhD7PDABnvrRf.html 2014年11月13日,微软表示开源.N ...
- ABP理论学习之功能管理
返回总目录 本篇目录 介绍 功能类型 定义功能 检查功能 功能管理者 版本说明 介绍 大多数的Saas(多租户)应用都有不同 功能的 版本(包).因此,他们可以给租户(客户)提供不同的 价格和功能选项 ...
- ReactJS入门(一)—— 初步认识React
React刚开始红的时候,由于对其不甚了解,觉得JSX的写法略非主流,故一直没打算将其应用在项目上,随着身边大神们的科普,才后知后觉是个好东西. 好在哪里呢?个人拙见,有俩点: 1. 虚拟DOM —— ...
- 剑指Offer面试题:25.二叉搜索树与双向链表
一.题目:二叉搜索树与双向链表 题目:输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的双向链表.要求不能创建任何新的结点,只能调整树中结点指针的指向.比如输入下图中左边的二叉搜索树,则输出转换之后的 ...