應用場景
先簡單描述一下標題的意思:使用 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 下載:
非常珍貴的參考資料: