zoukankan      html  css  js  c++  java
  • 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 下载:

    非常珍贵的参考资料:

  • 相关阅读:
    a标签点击之后有个虚线边框,怎么去掉
    在ie下,a标签包被img的时候,为什么有个蓝色的边线
    如何让一个变量的值,作为另一个变量的名字
    html 获取宽高
    两个同级div等高布局
    java中IO流异常处理
    连带滑块效果图
    java中File类的使用
    java保留两位小数4种方法
    java日历显示年份、月份
  • 原文地址:https://www.cnblogs.com/xishuai/p/ef-code-first-loading-filtered-child-collections-for-many-to-many.html
Copyright © 2011-2022 走看看