具有多个结果集的存储过程

有时,在使用存储过程时,需要返回多个结果集。 此方案通常用于减少组成单个屏幕所需的数据库往返次数。 在 EF5 之前,实体框架允许调用存储过程,但仅将第一个结果集返回给调用代码。

本文将介绍两种方法,可用于在实体框架中从存储过程访问多个结果集。 一种方法仅使用代码,Code First 和 EF 设计器均适用;另一种方法仅适用于 EF 设计器。 针对此操作的工具和 API 支持应该会在实体框架的未来版本中得到改进。

型号

本文中的示例使用基本的 Blog 和 Posts 模型,其中一篇博客有多个帖子,而一个帖子属于一篇博客。 我们将使用数据库中的存储过程来返回所有博客和帖子,如下所示:

    CREATE PROCEDURE [dbo].[GetAllBlogsAndPosts]
    AS
        SELECT * FROM dbo.Blogs
        SELECT * FROM dbo.Posts

使用代码访问多个结果集

我们可以执行 use 代码来发出原始 SQL 命令,以执行我们的存储过程。 这种方法的优点是它同时适用于 Code First 和 EF 设计器。

为了使多个结果集正常工作,我们需要通过 IObjectContextAdapter 接口存放到 ObjectContext API。

一旦有了 ObjectContext,就可以使用 Translate 方法将存储过程的结果转换为可以在 EF 中正常跟踪和使用的实体。 以下代码示例演示了此操作的实际效果。

    using (var db = new BloggingContext())
    {
        // If using Code First we need to make sure the model is built before we open the connection
        // This isn't required for models created with the EF Designer
        db.Database.Initialize(force: false);

        // Create a SQL command to execute the sproc
        var cmd = db.Database.Connection.CreateCommand();
        cmd.CommandText = "[dbo].[GetAllBlogsAndPosts]";

        try
        {

            db.Database.Connection.Open();
            // Run the sproc
            var reader = cmd.ExecuteReader();

            // Read Blogs from the first result set
            var blogs = ((IObjectContextAdapter)db)
                .ObjectContext
                .Translate<Blog>(reader, "Blogs", MergeOption.AppendOnly);   


            foreach (var item in blogs)
            {
                Console.WriteLine(item.Name);
            }        

            // Move to second result set and read Posts
            reader.NextResult();
            var posts = ((IObjectContextAdapter)db)
                .ObjectContext
                .Translate<Post>(reader, "Posts", MergeOption.AppendOnly);


            foreach (var item in posts)
            {
                Console.WriteLine(item.Title);
            }
        }
        finally
        {
            db.Database.Connection.Close();
        }
    }

Translate 方法接受我们在执行过程时收到的读取器、EntitySet 名称和 MergeOption。 EntitySet 名称将与派生上下文中的 DbSet 属性相同。 MergeOption 枚举控制内存中已存在相同实体时如何处理结果。

在这里,我们在调用 NextResult 之前循环访问博客集合,根据上面的代码,这一点很重要,因为在移动到下一个结果集之前必须使用第一个结果集。

调用这两个 translate 方法之后,EF 会以与任何其他实体相同的方式跟踪 Blog 和 Post 实体,因此可以正常修改或删除并保存这些实体。

注意

EF 在使用 Translate 方法创建实体时不考虑任何映射。 它会直接将结果集中的列名与类上的属性名称匹配。

注意

如果启用延迟加载,并访问其中一个 blog 实体上的 posts 属性,EF 将连接到数据库以延迟加载所有帖子,即使我们已经加载所有帖子也是如此。 这是因为 EF 不知道你是否已加载所有帖子或者数据库中是否还有更多帖子。 如果要避免这种情况,需要禁用延迟加载。

在 EDMX 中配置多个结果集

注意

必须面向 .NET Framework 4.5 才能在 EDMX 中配置多个结果集。 如果面向 .NET 4.0,则可以使用上一部分中所示的基于代码的方法。

如果使用 EF 设计器,还可以修改模型,以便它了解将返回的不同结果集。 需要事先了解的一点是,该工具无法识别多个结果集,因此你需要手动编辑 edmx 文件。 像这样编辑 edmx 文件会起作用,但也会中断 VS 中的模型验证。 因此,如果进行模型验证,你总是会收到错误。

  • 为此,你需要像处理单个结果集查询一样将存储过程添加到模型中。

  • 完成此操作后,你需要右键单击模型,依次选择“打开方式...”、“XML”

    Open As

将模型作为 XML 打开后,需要执行以下步骤:

  • 在模型中查找复杂类型和函数导入:
    <!-- CSDL content -->
    <edmx:ConceptualModels>

    ...

      <FunctionImport Name="GetAllBlogsAndPosts" ReturnType="Collection(BlogModel.GetAllBlogsAndPosts_Result)" />

    ...

      <ComplexType Name="GetAllBlogsAndPosts_Result">
        <Property Type="Int32" Name="BlogId" Nullable="false" />
        <Property Type="String" Name="Name" Nullable="false" MaxLength="255" />
        <Property Type="String" Name="Description" Nullable="true" />
      </ComplexType>

    ...

    </edmx:ConceptualModels>

 

  • 删除复杂类型
  • 更新函数导入,使其映射到实体,在我们的示例中,它将如下所示:
    <FunctionImport Name="GetAllBlogsAndPosts">
      <ReturnType EntitySet="Blogs" Type="Collection(BlogModel.Blog)" />
      <ReturnType EntitySet="Posts" Type="Collection(BlogModel.Post)" />
    </FunctionImport>

模型从中得知,存储过程将返回两个集合,一个包含 blog 条目,一个包含 post 条目。

  • 查找函数映射元素:
    <!-- C-S mapping content -->
    <edmx:Mappings>

    ...

      <FunctionImportMapping FunctionImportName="GetAllBlogsAndPosts" FunctionName="BlogModel.Store.GetAllBlogsAndPosts">
        <ResultMapping>
          <ComplexTypeMapping TypeName="BlogModel.GetAllBlogsAndPosts_Result">
            <ScalarProperty Name="BlogId" ColumnName="BlogId" />
            <ScalarProperty Name="Name" ColumnName="Name" />
            <ScalarProperty Name="Description" ColumnName="Description" />
          </ComplexTypeMapping>
        </ResultMapping>
      </FunctionImportMapping>

    ...

    </edmx:Mappings>
  • 将结果映射替换为返回的每个实体的结果,如下所示:
    <ResultMapping>
      <EntityTypeMapping TypeName ="BlogModel.Blog">
        <ScalarProperty Name="BlogId" ColumnName="BlogId" />
        <ScalarProperty Name="Name" ColumnName="Name" />
        <ScalarProperty Name="Description" ColumnName="Description" />
      </EntityTypeMapping>
    </ResultMapping>
    <ResultMapping>
      <EntityTypeMapping TypeName="BlogModel.Post">
        <ScalarProperty Name="BlogId" ColumnName="BlogId" />
        <ScalarProperty Name="PostId" ColumnName="PostId"/>
        <ScalarProperty Name="Title" ColumnName="Title" />
        <ScalarProperty Name="Text" ColumnName="Text" />
      </EntityTypeMapping>
    </ResultMapping>

也可以将结果集映射到复杂类型,例如默认创建的类型。 为此,你将创建一个新的复杂类型,而不是删除它们,并在上面示例中使用过实体名称的所有位置使用复杂类型。

更改这些映射后,可以保存模型,并执行以下代码以使用存储过程:

    using (var db = new BlogEntities())
    {
        var results = db.GetAllBlogsAndPosts();

        foreach (var result in results)
        {
            Console.WriteLine("Blog: " + result.Name);
        }

        var posts = results.GetNextResult<Post>();

        foreach (var result in posts)
        {
            Console.WriteLine("Post: " + result.Title);
        }

        Console.ReadLine();
    }

注意

如果手动编辑模型的 edmx 文件,一旦从数据库重新生成模型,该文件就会被覆盖。

总结

我们在这里展示了使用实体框架访问多个结果集的两种不同方法。 这两种方法同样有效,具体取决于你的情况和偏好。你应该选择最适合自身情况的方法。 根据计划,对多个结果集的支持将在未来的实体框架版本中得到改进,并且不再需要执行本文档中的步骤。