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

时间:2014-02-03 21:59:29

标签: c# sql sql-server linq 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);

但是,在将此代码转换为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);
}

进行此更改后,我现在遇到错误“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();

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;
}

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

在对这些错误进行广泛研究后,我仍无法找到与我的问题有关的解决方案。

问题

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

更新

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

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

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

  3. 更新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();
    

    但我不能这样做:

    (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();
    

3 个答案:

答案 0 :(得分:3)

它可以以相当简单的方式完成,但需要一些手动操作。 Here is an MSDN post处理具有多个结果集的存储过程,这些结果集显示代码优先和数据库优先方法。

示例:

加载EntityB proc:

create proc dbo.Get_EntityB_by_EntityAId( @aId int )
as

select distinct
    b.EntityBId
    , b.Description
from
    EntityA a
    left outer join EntityB b
     on a.PrimaryEntityB_EntityBId = b.EntityBId
    left outer join EntityB b2
     on a.AlternativeEntityB_EntityBId = b2.EntityBId
where
    a.EntityAId = @aId
go

加载EntityA proc(调用load B proc)

create proc dbo.Get_EntityA_by_Id( @id int )
as

-- use a select statement
select 
    a.EntityAId
    , a.Description
    , a.PrimaryEntityB_EntityBId
    , a.AlternativeEntityB_EntityBId
from
    EntityA a
where
    a.EntityAId = @id

-- and/or other sprocs
exec dbo.Get_EntityB_by_EntityAId @id

go

实体类

[Table("EntityA")]
public partial class EntityA
{
    public int EntityAId { get; set; }
    public string Description { get; set; }


    public virtual EntityB PrimaryEntityB { get; set; }

    public virtual EntityB AlternativeEntityB { get; set; }
}


[Table("EntityB")]
public partial class EntityB
{
    public int EntityBId { get; set; }
    public string Description { get; set; }

    [InverseProperty("PrimaryEntityB")]
    public virtual ICollection<EntityA> EntityAsViaPrimary { get; set; }
    [InverseProperty( "AlternativeEntityB" )]
    public virtual ICollection<EntityA> EntityAsViaAlternative { get; set; }
}

调用sproc并处理结果的方法(对于此方法,如果您愿意,可以返回EntityA

public static void EagerLoadEntityA( int aId )
{
    using( var db = new TestEntities() )
    {
        // if using code first
        db.Database.Initialize( false );

        var cmd = db.Database.Connection.CreateCommand();
        cmd.CommandText = "dbo.Get_EntityA_by_Id";

        db.Database.Connection.Open();

        try
        {
            var reader = cmd.ExecuteReader();

            var objContext = ( ( IObjectContextAdapter )db ).ObjectContext;

            var aEntities = objContext
                .Translate<EntityA>( reader, "EntityAs", MergeOption.AppendOnly );

            reader.NextResult();

            var bEntities = objContext
                .Translate<EntityB>( reader, "EntityBs", MergeOption.AppendOnly );

        }
        finally
        {
            db.Database.Connection.Close();
        }
    }
}

用法:

EagerLoadEntityA( 1234 );
var entityA = db.EntityAs.Find( 1234 ); // cached
var primB = entityA.PrimaryEntityB; // this is already loaded

答案 1 :(得分: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.

答案 2 :(得分:0)

为了选择/读取数据,存储过程不能与实体直接关联。通常,用于检索的存储过程将返回复杂类型而不是实体。为了间接与DB交互,EF提供了将实体与View相关联的能力,以及Insert,Update和Delete的存储过程。

阅读本文以获取使用EF和存储过程的完整摘要。 http://msdn.microsoft.com/en-us/data/gg699321.aspx