【问题标题】:Return final result from stored procedure in Entity Framework从实体框架中的存储过程返回最终结果
【发布时间】:2016-12-11 17:57:21
【问题描述】:

我有一个带有 SQL Server 后端的 WebAPI 服务,它使用实体框架(代码优先)进行管理。我使用 Linq 查询从数据库中检索数据。由于我的一些 API 端点执行多个连接来获取数据,因此在生产中部署时我观察到了一些延迟。我阅读了使用 ORM 映射与直接 SQL 查询(存储过程)并得出结论,对于复杂查询,使用存储过程是最佳选择。

我看到几篇文章解释了如何使用如下代码从存储过程中返回值:

context.Database.SqlQuery<T>(...)

但是,所有示例都处理简单的 SELECT 查询,是的,我也遇到了返回多个 SELECT 查询结果。但在这种情况下,它是 2 个SELECT 查询。

在实际场景中,可能会涉及到 2 个以上的 select 查询,因为它可能涉及到 join 和其他代码。而且,在查询结束时,我们将推导出最终的SELECT 查询,它返回预期的数据。下面是一个示例模型代码来解释这个场景:

// Inside my stored procedure
Declare @XId uniqueidentifier
Declare @YId uniqueidentifier

// Some mockup queries. This can have JOINS and other complex codes in real code.
// Below code is made as simple as possible to explain the scenario
Select @XId=Id From Table1 Where Name = 'Name1'
Select @YId=Id From Table2 Where Code = 'Code1'

// This is the data, I am interested
Select * From Table3 Where Col1Id=@XId And Col2Id=@YId

如何从最后一个 SELECT 查询中获取行以实现此目的?

我做了一些研究并假设它不可能使用 EF,因为存储过程支持很少。这是否可以在 EF 限制内完成,或者我是否需要为此回退到 ADO.NET?

任何帮助将不胜感激。

谢谢。

【问题讨论】:

  • @GertArnold - 我添加了示例代码 sn-p 来解释场景。
  • 我认为您应该将Select * 替换为Select [your actual columns list],这样您才能真正知道自己在使用什么。然后创建一个类型MyResultType,其属性与所选列匹配并调用context.Database.SqlQuery&lt;MyResultType&gt;(...)
  • 是的,我已经尝试过了,但它正在转换为 Table1DTO 而不是 Table3DTO,这当然会引发转换错误。

标签: c# entity-framework linq


【解决方案1】:

在阅读了 MSDN 文章 - https://msdn.microsoft.com/en-us/data/jj691402.aspx 后,我设法推导出了一个解决方案。这是我所做的。

我使用DbCommand.ExecuteReader() 执行存储过程,它返回DbDataReader。因为,我的兴趣点是最后一个 SELECT 声明。我设法跳到DbCommandReader 的最后一个结果集,并使用ObjectContext.Translate&lt;T&gt;() 将其转换为我的DTO

不确定是否有更好的可选选项。暂时有效。

更新:我终于切换到 Dapper,这是 StackExchange 的一个微型 ORM。与 EF 相比,它显着提升了性能。使用 EF 耗时 3 秒的 API 调用现在在不到 500 毫秒内返回数据。

谢谢。

【讨论】:

  • 我在写答案时也在看那篇文章。使用对象上下文肯定有助于我调试,因为它的方法抛出了一些更有用的异常。但是一旦问题得到解决,我认为SqlQuery 是比返回对象上下文更干净的解决方案。
  • 您对哪种性能实现方式有什么建议吗?在高层次上,由于我正在处理ExecuteReader(),我可以精确地选择我需要转换为类型TSELECT 语句。如果我选择SqlQuery,那么系统需要将返回类型T 与每个Select 语句进行比较,以在强制转换之前查看哪个是匹配的。这有任何性能问题吗?有什么想法吗?
  • 实际上,由于您只对最后一个查询感兴趣,您可能需要重新设计查询,使其成为唯一返回结果的查询。也许看看With - As - Select 子句来替换您的过程的内部预选。将在我的问题中编辑一个 SP 示例。
【解决方案2】:

由于您没有透露太多代码,我只能为您提供一个工作示例。

假设您有以下 CodeFirst 架构

class DbC : DbContext
{
    public DbC()
    {
    }
    public DbSet<Blog> Blogs { get; set; }
}

class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public List<Post> Posts { get; set; }
}

class Post
{
    public int Id { get; set; }
    public string Text { get; set; }
}

我假设存在于数据库中的一些数据:

var tmp = new List<Blog>()
{
    new Blog
    {
        Name = "Blog1",
        Posts = new List<Post>()
        {
            new Post { Text = "Post1" },
            new Post { Text = "Post2" },
            new Post { Text = "Post3" },
        }
    },
    new Blog
    {
        Name = "Blog2",
        Posts = new List<Post>()
        {
            new Post { Text = "Post4" },
            new Post { Text = "Post5" },
            new Post { Text = "Post6" },
        }
    },
};

还有一个通过Blog.Name检索Posts的存储过程

CREATE PROCEDURE [dbo].[PostsRetrieverProc]
    @BlogName NVARCHAR (MAX)
AS
    Declare @BlogId int = 0

    Select @BlogId=Id From [Blogs] Where Name = @BlogName

    Select
        b.Id [BlogId],
        p.Id [PostId],
        p.[Text] [PostText]
    From [Blogs] b join [Posts] p On b.Id = p.Blog_Id
    Where p.Blog_Id =  @BlogId
GO

然后您可以创建一个任意类,其属性的名称和类型与您的过程结果匹配

public class MyCustomReturnType
{
    public int BlogId { get; set; }
    public int PostId { get; set; }
    public string PostText { get; set; }
}

并调用存储过程返回一些值

using (var db = new DbC())
{
    var result = db.Database.SqlQuery<MyCustomReturnType>("dbo.PostsRetrieverProc @param1", new SqlParameter("param1", "Blog2"));
    foreach (var item in result)
    {
        Console.WriteLine(item.PostText);
    }
}

我的结果输出如预期:

Post4
Post5
Post6

这种方法有什么问题吗?

编辑:

由于您只需要存储过程的最后一个结果,因此最好重新设计 SP,而不是跳过中间结果。这可以通过With clause 完成。请注意,此示例代码有点臃肿,因为我只使用博客 ID 并仍然加入 [Blogs] 只是为了表明它是可能的

CREATE PROCEDURE [dbo].[PostsRetrieverProc]
    @BlogName NVARCHAR (MAX)
AS
    With
        BlogEntries (BlogId)
    As
        (Select Id BlogId From [Blogs] Where Name = @BlogName)
    Select
        b.Id [BlogId],
        p.Id [PostId],
        p.[Text] [PostText]
    From BlogEntries e
    join [Blogs] b On e.BlogId = b.Id
    join [Posts] p On b.Id = p.Blog_Id

允许多个预选也很有趣:Can I use multiple "with"?

【讨论】:

  • 这段代码有效吗?事实上,我昨天尝试了同样的事情,但它对我没有用。让我对你的样品做同样的尝试。谢谢。
  • 我必须格外小心,在 SP 中为 BlogId 分配默认值,以便在命令字符串中包含 SP 参数(不要依赖具有相同名称的 SqlParameter),但是,是的,我真的执行了这个,它对我有用,没有假设的例子。
  • 我试了样例。这是我得到的。 ErrorMessage=An error has occurred., The data reader is incompatible with the specified 'MY_TYPE_NAME_HERE'. A member of the type, 'PROPERTY_NAME_HERE', does not have a corresponding column in the data reader with the same name., at System.Data.Entity.Core.Query.InternalTrees.ColumnMapFactory.GetMemberOrdinalFromReader(DbDataReader storeDataReader, EdmMember member, EdmType currentType, Dictionary2 renameList)`.
  • 你真的用新的数据库(文件或其他)创建了一个新的项目,默认System.Data.SqlClient作为提供者,没有什么特别的事情要搞砸,复制存储过程生成代码没有任何变化?然后在 VS 中打开服务器资源管理器,找到你的数据库,新建一个查询表并执行dbo.PostsRetrieverProc 'Blog2' 以验证 SP 是否在 SQL 中工作,然后再寻找其他潜在的问题源。检查结果并在检查结果列标题名称时非常迂腐,例如打字错误,对MyCustomReturnType进行同样的检查。
【解决方案3】:

不要使用只是执行。

使用这个 sql:

Insert into #tempTable 
 execute  sp_executesql @SELECT

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-11-15
    • 2015-02-05
    • 2021-04-08
    • 2012-05-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多