应用场景

先简单描述一下标题的意思:使用 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 下载:

非常珍贵的参考资料:

EF Code First 一对多、多对多关联,如何加载子集合?的更多相关文章

  1. 多对多关联懒加载导致failed to lazily initialize a collection of role: 实体类, could not initialize proxy - no Session 追加配置fetch = FetchType.EAGER解决

    一篇文章需要关联很多个标签,所以他们呈一对多(多对多)的关系 org.springframework.web.util.NestedServletException: Request processi ...

  2. Entity Framework Code First实体关联数据加载

    在项目过程中,两个实体数据之间在往往并非完全独立的,而是存在一定的关联关系,如一对一.一对多及多对多等关联.存在关联关系的实体,经常根据一个实体的实例来查询获取与之关联的另外实体的实例. Entity ...

  3. 第五节: EF高级属性(一) 之 本地缓存、立即加载、延迟加载(不含导航属性)

    一. 本地缓存 从这个章节开始,介绍一下EF的一些高级特性,这里介绍的首先介绍的EF的本地缓存,在前面的“EF增删改”章节中介绍过该特性(SaveChanges一次性会作用于本地缓存中所有的状态的变化 ...

  4. MyBatis 一对多,多对一关联查询的时候Mapper的顺序

    要先写association,然后写collection:这是由DTD决定的: <resultMap ...> <association ...> </associati ...

  5. EF连接Sqlserver2014,使用DBGeography时提示无法加载sqlserverspatial.dll

    (1)确认你要使用的SqlServer版本,如果是2014,就要在nuget中添加microsoft.sqlserver.types.dll,使用12.0.4100.1这个版本,它会自动添加sqlse ...

  6. 多表关联懒加载导致的org.hibernate.LazyInitializationException: could not initialize proxy - no Session

    本来考虑的是懒加载,这样可以提高效率,不过由于时间紧急 把懒加载改为急加载临时解决 https://www.jianshu.com/p/89520964f458 自己管理session也可以 临时补丁 ...

  7. Entity Framework关联实体的三种加载方法

    推荐文章 EF性能之关联加载 总结很好 一:介绍三种加载方式 Entity Framework作为一个优秀的ORM框架,它使得操作数据库就像操作内存中的数据一样,但是这种抽象是有性能代价的,故鱼和熊掌 ...

  8. EF Code First一对一、一对多、多对多关联关系配置

    1.EF Code First一对一关联关系 项目结构图: 实体类: Account.cs using System; using System.Collections.Generic; using ...

  9. mybatis的执行流程 #{}和${} Mysql自增主键返回 resultMap 一对多 多对一配置

    n Mybatis配置 全局配置文件SqlMapConfig.xml,配置了Mybatis的运行环境等信息. Mapper.xml文件即Sql映射文件,文件中配置了操作数据库的Sql语句.此文件需要在 ...

随机推荐

  1. Android带边框表格的实现

    最近做项目需要用到表格,数据是动态指定的,本来用GridView是很方便的,可是老大不同意用这么重量级的控件,想办法吧. 做表格很容易想到用TableLayout,那就自定义一个来搞. 一.表格最蛋疼 ...

  2. DBUtils 笔记

    一.DBUtils介绍  apache 什么是dbutils,它的作用 DBUtils是java编程中的数据库操作实用工具,小巧简单实用. DBUtils封装了对JDBC的操作,简化了JDBC操作.可 ...

  3. 解决bootstrap模态框内输入框无法获取焦点

    bootstrap 模态框中的input标签在某些情况下会无法获取焦点. 最终解决方法:去除模态框的 tabindex="-1" 属性即可

  4. 解决WARN: Timeout/setRollbackOnly of ACTIVE coordinator !的问题

    该问题是CoordinatorImp上面的一个定时器造成的,一个活动的session如果在一定的时间内没有执行完毕就会rollback,就算没有sql执行也会不断的进行. 可以参考该链接:https: ...

  5. 浅谈Js对象的概念、创建、调用、删除、修改!

    一.我们经常困惑,对象究竟是什么,其实这是一种思维,一种意识上的东西,就像我们都说    世界是有物质组成的道理一样,理解了下面的几句话!对象也不是那么抽象!    1.javascript中的所有事 ...

  6. Wine——在Linux上运行Windows软件

    官网:https://www.winehq.org/ 参考: wikipedia 教你使用Wine在Linux上运行Windows软件 如何安装和使用Wine,以便在Linux上运行Windows应用 ...

  7. 【转】Artificial Neurons and Single-Layer Neural Networks

    原文:written by Sebastian Raschka on March 14, 2015 中文版译文:伯乐在线 - atmanic 翻译,toolate 校稿 This article of ...

  8. 细数iOS上的那些安全防护

    细数iOS上的那些安全防护  龙磊,黑雪,蒸米 @阿里巴巴移动安全 0x00 序 随着苹果对iOS系统多年的研发,iOS上的安全防护机制也是越来越多,越来越复杂.这对于刚接触iOS安全的研究人员来说非 ...

  9. 理解C# 4 dynamic(3) – DynamicObject的使用

    上篇文章"理解C# 4 dynamic(2) – ExpandoObject的使用" 了解了ExpandoObject的基本使用. 但ExpandoObject的问题就是它是一个万 ...

  10. 【数据结构】平衡二叉树—AVL树

    (百度百科)在计算机科学中,AVL树是最先发明的自平衡二叉查找树.在AVL树中任何节点的两个子树的高度最大差别为一,所以它也被称为高度平衡树.查找.插入和删除在平均和最坏情况下都是O(log n).增 ...