【问题标题】:Entity Framework - Eagerly load object graph using stored procedures实体框架 - 使用存储过程急切加载对象图
【发布时间】:2014-02-03 21:59:28
【问题描述】:

背景

我正在将项目中的 LINQ-to-SQL 代码更改为实体框架。大部分的改变都相对简单,但是,我遇到了一个相当大的问题。使用 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 认为整个对象图是由相同的上下文加载的,所以我将我的代码重写为:

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 的所有者为空时,不允许请求的操作。使用默认构造函数创建的 RelatedEnd 对象只能在序列化期间用作容器。 ”。

在深入研究了这些错误中的每一个后,我仍然无法找到与我的问题相关的解决方案。

问题

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

更新

因此,根据目前的回答,我觉得我需要在此处包含以下注意事项:

  1. 我不是在寻找使用单个存储过程来加载整个对象图的答案。我正在寻找一种使用每个实体获取存储过程来加载对象图的方法。我意识到使用单个存储过程加载对象图理论上可以执行得更好,但此时,我对代码库的较小更改更感兴趣,尤其是在数据库结构方面。

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

但我做不到:

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

【问题讨论】:

    标签: c# sql sql-server linq entity-framework


    【解决方案1】:

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

    例子:

    加载 EntityB 过程:

    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
    

    【讨论】:

    • 感谢您的帖子,虽然您的解决方案是有效的解决方案(从单个存储过程加载整个对象图),但我更专注于从我已经存储的过程集中加载整个对象图有。您能否提供一个与我的问题中的代码示例同义的示例?此外,编辑自动生成的 edmx 代码是不可接受的。我已经能够在其他 ORM 中实现这一点,所以如果我不能使用 EF 完成同样的事情,那就太令人困惑了。
    • 单个存储过程可以调用其他存储过程,每个存储过程可以返回1个或多个数据集。
    • 我意识到你说的是真的,但是,这将意味着整个数据库的巨大变化为我需要加载的每个对象图创建一个存储过程,无论这些存储过程是否调用现有的一个与否。我正在寻找一个只更改代码的解决方案,因为我所做的只是切换 ORM,如果 ORM 擅长作为 ORM,我不应该进行数据库更改来适应它。
    • 在您发布的代码中,EntityB 应该有一个 List C 属性。虽然,我想知道是否可以使用您的手动方法来加载整个对象图。唯一的问题是,使用这种方法,我觉得我还不如使用普通的 ADO.NET,因为我实际上从 ORM 中获得的唯一好处就是模型代码生成,我可以用好的 ol ' 类型化的数据集。如果这是让它工作的唯一方法,我应该选择另一个 ORM 或将其保留为 Linq-to-SQL。
    • 你做什么取决于你,我只是证明了它可以用 EF 有效地完成
    【解决方案2】:

    好的,经过进一步考虑,我想出了一个适合我想要的解决方案。由于我在 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.
    

    【讨论】:

    【解决方案3】:

    为了选择/读取数据,存储过程不能直接与实体关联。通常用于检索的存储过程将返回复杂类型而不是实体。为了与 DB 间接交互,EF 提供了将实体与视图关联以进行读取以及用于插入、更新和删除的存储过程的能力。

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

    【讨论】:

    • 如果这是真的,为什么 EF 会让您能够将函数导入配置为返回指定实体的集合?此外,您引用的文章说,EF 完全支持对所有内容使用存储过程的路径,尽管它没有提供从多个存储过程加载整个对象图的示例。我意识到使用单个存储过程在性能方面可能会更好,但此时,我更多地关注代码的可维护性和我正在进行的转换。
    • True EF 使您能够将函数导入返回值映射到存储过程,但它本身不会将其理解为实体对象图的一部分。 EF 原生支持数据库映射和完整对象图构建的方式是使用映射表,或使用映射视图进行读取,以及映射存储过程用于插入、更新和删除。
    • 这并不是说没有其他方法可以编码并完全利用存储过程来获得单个对象图。我只是说目前 EF 本身并不支持此功能。
    • 这个答案很好地解释了为什么 EF 不支持存储过程来选择实体的功能。 stackoverflow.com/a/4108651/24908
    • 我理解您引用的答案中的要点,但我不明白为什么它不能将返回实体列表的存储过程视为该实体的投影并能够组合几个一起使用 Linq 连接。就像我对 moho 说的那样,我无法想象不能使用 Linq-to-SQL 完成如此轻松的事情,尤其是在 Microsoft 放弃了 Linq-to-SQL 以支持 EF 之后。我尝试使用 MergeOption.NoTracking,但仍然无法诱使 EF 认为实体已加载,即使它不是通过 IQueryable 接口进行的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-09
    • 1970-01-01
    • 2012-02-07
    • 1970-01-01
    相关资源
    最近更新 更多