2

I have a WebAPI service with a SQL Server backend which is managed using Entity Framework (code-first). I have used Linq queries to retrieve data from the database. Since a few of my API endpoints do multiple joins to get data, I observed some delay when deployed in production. I read on using ORM mapping vs direct SQL queries (stored procedures) and came to a conclusion that for complex queries, it's optimal to use stored procedures.

I came across few articles explaining how to return value from stored procedure using code like:

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

However, all the examples deals with simple SELECT query and yes, I also came across returning multiple SELECT query results too. But in that case, it's 2 SELECT queries.

In real scenario, it can be more than 2 select queries since it may involves joins and other codes. And, at the end of the query only we'll be deducing the final SELECT query which returns the expected data. Below is a sample mock-up code to explain the scenario:

// 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

How can I get rows from last SELECT query achieve this?

I did some research and assume that it's not possible to do with EF since stored procedure support is minimal. Is that possible to do within EF limits or do I need to fall back to ADO.NET for this?

Any help will be appreciated.

Thanks.

abhilashca
  • 83
  • 2
  • 9
  • @GertArnold - I have added sample code snippet explaining the scenario. – abhilashca Dec 12 '16 at 04:47
  • I think you should replace `Select *` with `Select [your actual columns list]` so you actually know what you are working with. Then create a type `MyResultType` whos properties match the selected columns and call `context.Database.SqlQuery(...)`. – grek40 Dec 12 '16 at 09:23
  • Yes, I have already tried that but it's casting to `Table1DTO` and not `Table3DTO` which of course throws casting error. – abhilashca Dec 12 '16 at 13:39

3 Answers3

1

I somehow managed to deduce a solution after reading the MSDN article - https://msdn.microsoft.com/en-us/data/jj691402.aspx. Here is what I had done.

I executed stored procedure using DbCommand.ExecuteReader() which return DbDataReader. Since, my point-of-interest is the last SELECT statement. I managed to skip to the last resultset of DbCommandReader and casted that to my DTO using ObjectContext.Translate<T>().

Not sure whether there is any better optional available. For time being it works.

Update: I finally switched to Dapper which is a micro ORM from StackExchange. It gives significant performance boost when compared with EF. The API call that took 3 sec with EF is now returning data in less than 500ms.

Thanks.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
abhilashca
  • 83
  • 2
  • 9
  • I was also looking at that article when I wrote my answer. Using the object context was definitely helping me at debugging because its methods were throwing some more helpful exceptions. But once the issues are sorted out, I think `SqlQuery` is the cleaner solution compared to going back to object context. – grek40 Dec 12 '16 at 15:56
  • Do you have any suggestion on which is the performance way to implement? On a high-level, since I am dealing with `ExecuteReader()` I could precisely pick the `SELECT` statement which I need to cast to type `T`. If I choose, `SqlQuery` then system needs to compare the return-type `T` with each `Select` statement to see which is the matching one prior to casting. Does this has any performance issues? Any thoughts? – abhilashca Dec 14 '16 at 09:03
  • Actually, since you are only interested in the last query, you may want to redesign the query to be the only one thats returning results. Maybe have a look at the [`With - As - Select`](https://msdn.microsoft.com/en-us/library/ms175972.aspx) clause to replace the internal pre-selects of your procedure. Gonna edit a SP example into my question. – grek40 Dec 14 '16 at 09:33
0

Since you didn't disclose much of your code, I can only provide you a working sample.

Suppose you have the following CodeFirst schema

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

A few data that I assume to reside within the database:

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

And a stored procedure to retrieve Posts by Blog.Name

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

Then you can create an arbitrary class with properties whose names and types match your procedure result

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

And call the stored procedure to return some values

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

My result output as expected:

Post4
Post5
Post6

Any problems with the approach?

Edit:

Since you only want the last result from your stored procedure, it might be better to redesign the SP instead of skipping the intermediate results. This can be done with the With clause. Note that this example code is a bit bloated, since I only use the blog Id and still join with [Blogs] just to show that its possible

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

Also interesting to allow multiple pre-selects: Can I use multiple "with"?

Community
  • 1
  • 1
grek40
  • 13,113
  • 1
  • 24
  • 50
  • Does this code works? I infact tried the same thing last day but it didn't worked for me. Let me try the same with your sample. Thanks. – abhilashca Dec 12 '16 at 15:45
  • I had to be extra careful to assign default value to `BlogId` in the SP, to include the SP parameters in the command string (don't rely on `SqlParameter` with the same name), but yes, I really executed this and it worked for me, no hypothetical example. – grek40 Dec 12 '16 at 15:52
  • I tried the sample. Here is what I got. `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, Dictionary`2 renameList)`. – abhilashca Dec 12 '16 at 16:20
  • You really created a fresh project with a fresh database (file or whatever), default `System.Data.SqlClient` as provider, no special things to mess up, copied the stored procedure generation code without any change? Then open the server explorer in VS, find your database, create a new query sheet and execute `dbo.PostsRetrieverProc 'Blog2'` to verify that the SP is working in SQL before looking for other potential problem sources. Inspect the result and be very pedantic at checking the result column header names for anything like typing errors, do the same check for `MyCustomReturnType`. – grek40 Dec 12 '16 at 17:14
0

do not use just execute.

use this sql:

Insert into #tempTable 
 execute  sp_executesql @SELECT
MJK
  • 3,434
  • 3
  • 32
  • 55
Ram
  • 1