实体框架 - 使用存储过程急切地加载对象图

Aar*_*ins 6 c# sql linq sql-server entity-framework

背景

我正在将项目中的LINQ-to-SQL代码更改为Entity Framework.大部分的转变都相对简单,但是,我遇到了一个相当重要的问题.使用LINQ-to-SQL,我能够使用如下存储过程加载整个对象图(对于模型B):

ViewModel.Model = MyDbContext.usp_ModelA_GetByID(AId).Single();
List<ModelB> Details = 
    (from b in MyDbContext.usp_ModelB_GetByID(BId)
    join c in MyDbContext.usp_ModelC_GetAll()
       on b.CId equals c.CId
    select new ModelB()
    {
        BId = b.BId,
        CId = b.CId,
        C = c
    }).ToList();
ViewModel.Model.ModelBs.AddRange(Details);
Run Code Online (Sandbox Code Playgroud)

但是,在将此代码转换为EF后,在访问ViewModel.Model.ModelBs的行上,我得到错误"EntityCommandExecutionException",内部异常解释"对象'ModelBTable'上的SELECT权限被拒绝." 显然,EF正在尝试为ModelA获取ModelB,即使我已经从数据库中加载了它们.虽然我不完全理解为什么它试图加载实体,即使我已经添加它们,我只能假设因为它本身没有加载它们,它不相信它们是完全加载的并且可能会查看所有的对象我作为"新"加载到它.

为了绕过EF试图获取对象本身,我决定将我的代码更改为:

ViewModel.Model = MyDbContext.usp_ModelA_GetByID(AId).Single();
List<ModelB> Details = 
    (from b in MyDbContext.usp_ModelB_GetByID(BId)
    join c in MyDbContext.usp_ModelC_GetAll()
        on b.CId equals c.CId
     select new ModelB()
     {
        BId = b.BId,
        CId = c.CId,
        C = c
     }).ToList();
ViewModel.Model.ModelBs = new EntityCollection<ModelB>();
foreach (ModelB detail in Details)
{
    ViewModel.Model.ModelBs.Attach(detail);
}
Run Code Online (Sandbox Code Playgroud)

进行此更改后,我现在遇到错误"InvalidOperationException",并显示消息"无法初始化EntityCollection,因为EntityCollection所属对象的关系管理器已附加到ObjectContext .InitializeRelatedCollection方法应该只是在对象图的反序列化期间调用以初始化新的EntityCollection."

这是令人困惑的,因为我使用相同的上下文来加载所有实体,所以我不确定为什么它不允许我将它们组合在一起.我能够在没有问题的情况下在其他ORM中执行此操作.

在研究了这个错误之后,我决定尝试一种方法,我希望EF会欺骗EF认为整个对象图由相同的上下文加载,所以我重写了我的代码:

ViewModel.Model = 
    (from a in MyDbContext.usp_ModelA_GetByID(AId)
    select new A()
    {
        AId = a.AId,
        ModelBs = (from b in MyDbContext.usp_ModelB_GetByID(BId)
                  join c in MyDbContext.usp_ModelC_GetAll()
                      on b.CId equals c.CId
                  select new ModelB()
                  {
                      BId = b.BId,
                      CId = b.CId,
                      C = c
                  }).ToEntityCollection()
    }).Single();
Run Code Online (Sandbox Code Playgroud)

ToEntityCollection是我创建的扩展方法,如下所示:

public static EntityCollection<TEntity> ToEntityCollection<TEntity>(
     this IEnumerable<TEntity> source) where TEntity : class, IEntityWithRelationships
{
    EntityCollection<TEntity> set = new EntityCollection<TEntity>();
    foreach (TEntity entity in source)
    {
        set.Attach(entity);
    }
    return set;
}
Run Code Online (Sandbox Code Playgroud)

现在,我收到错误"InvalidOperationException",并显示消息"当此RelatedEnd的所有者为null时,不允许请求操作.使用默认构造函数创建的RelatedEnd对象应仅在序列化期间用作容器.".

在广泛研究了这些错误之后,我仍然无法找到与我的问题有关的解决方案.

所以,经过所有这些,我的问题是:当每个对象使用Entity Framework 4拥有自己的存储过程时,如何加载整个对象图?

更新

所以,基于到目前为止的答案,我觉得我需要在这里包含以下警告:

  1. 我不是在寻找使用单个存储过程来加载整个对象图的答案.我正在寻找一种方法来使用每个实体的get存储过程加载对象图.我意识到使用单个存储过程加载对象图可以在理论上表现得更好,但此时,我对代码库的较小更改更感兴趣,特别是关于数据库结构的方式.

  2. 如果您的解决方案需要直接编辑edmx,那么这将不是一个可接受的答案.由于这是一个自动生成的文件,因此直接编辑edmx实际上意味着在通过设计器进行任何修改时需要重新完成相同的更改.

更新2

因此,经过一番商议,我想出了一个解决方案.我所做的是将我的ViewModel更改为具有List ModelBs属性,该属性使用存储过程连接来提取数据,在我的视图中,我只是将此属性设置为数据源.这绝对不是我认为的最佳解决方案,因为现在我的ViewModel更像是Model而不是ViewModel,我不能再遍历我的ModelA类型来获取ModelB列表,但它可以工作!我仍然不明白为什么我能这样做:

(from b in MyDbContext.usp_ModelB_GetByID(BId)
join c in MyDbContext.usp_ModelC_GetAll()
    on b.CId equals c.CId
select new ModelB()
{
    BId = b.BId,
    CId = b.CId,
    C = c //<------Setting a navigation property and EF figures out that it belongs
}).ToList();
Run Code Online (Sandbox Code Playgroud)

但我做不到:

(from a in MyDbContext.usp_ModelA_GetByID(AId)
select new ModelA()
{
    AId = a.AId,
    ModelBs = MyDbContext.usp_ModelB_GetByID(BId).ToEntityCollection() //<----Won't let me set the navigation property when the navigation property is a collection.
}).Single();
Run Code Online (Sandbox Code Playgroud)

Aar*_*ins 1

好吧,经过进一步的考虑,我找到了一个适合我想要的解决方案。由于我处于 Web 环境中,不需要延迟加载对象,因此我将整个 DbContext 的 EnableLazyLoading 设置为 false。然后,使用称为神奇关系修复的 EF 功能,我能够执行以下操作:

ViewModel.Model = MyDbContext.usp_ModelA_GetByID(AId).Single();
var Details = 
(from b in MyDbContext.usp_ModelB_GetByID(BId)
join c in MyDbContext.usp_ModelC_GetAll()
   on b.CId equals c.CId
select new ModelB()
{
    BId = b.BId,
    CId = b.CId,
    C = c
}).ToList();  
//ToList() executes the proc and projects the plate details into the object 
//graph which never tries to select from the database because LazyLoadingEnabled is
//false.  Then, the magical relationship fix-up allows me to traverse my object graph
//using ViewModel.Model.ModelBs which returns all of the ModelBs loaded into the graph
//that are related to my ModelA.
Run Code Online (Sandbox Code Playgroud)