Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Debugging Entity Framework queries

This is a bit of subjective question about a specific situation. Main goal for this question for me is to remind my self to code up the solution. However if there is already a solution, or an alternate approach, I would like to know it.

I'm working on a project and I'm using Entity Framework 4 for database access. The database design is something that I don't have control over. The database was designed many years ago, and in my opinion the database design does not fit for the current database purposes. This results in very complicated queries.

This is the first time I'm using Entity Framework in a project, but I have extensive experience in development against MS SQL Server.

What I found myself doing again and again is this:

  • I write a complex L2E query. The query either slow or returns wrong results
  • I'm looking at my L2E query and I have absolutely no idea how to improve it
  • I fire up SQL Profiler and capture the SQL that EF generated from my query
  • I want to execute part of that sql to identify the part of the query that is giving problems
  • The query comes through as sp_executesql with a dozen of parameters, because if a parameter is used 3 times in a query, L2E creates 3 parameters and passes to all of them the same value. Same deal with every parameter.
  • Now I have to extract the SQL from sp_executesql, unescape all escaped apostrophes, and substitute every parameter in the query with its value
  • After this is done I finally can run parts of the query and pin-point the problem.
  • I go back to my L2E code, change it to fix the problem I found and the cycle repeats.

To be honest, I'm starting thinking that one should not use an ORM if you don't own database design.

This aside, the process of unescaping the sql and substituting the parameters is the one that I want to automate. The goal is to get 'naked', de-parametrized sql, that I can run in SSMS.

This a very simple example of what I see in the profile and what I want to get in result. My real cases are many times more complex.

The capture:

exec sp_executesql N'SELECT 
[Extent1].[ProductName] AS [ProductName]
FROM  [dbo].[Products] AS [Extent1]
INNER JOIN [dbo].[Categories] AS [Extent2] ON [Extent1].[CategoryID] = [Extent2].[CategoryID]
WHERE ([Extent1].[UnitPrice] > @p__linq__0) AND ([Extent2].[CategoryName] = @p__linq__1) AND (N''Chang'' <> [Extent1].[ProductName])',N'@p__linq__0 decimal(1,0),@p__linq__1 nvarchar(4000)',@p__linq__0=1,@p__linq__1=N'Beverages'

Desired result:

SELECT 
[Extent1].[ProductName] AS [ProductName]
FROM  [dbo].[Products] AS [Extent1]
INNER JOIN [dbo].[Categories] AS [Extent2] ON [Extent1].[CategoryID] = [Extent2].[CategoryID]
WHERE ([Extent1].[UnitPrice] > 1) AND ([Extent2].[CategoryName] = N'Beverages') AND (N'Chang' <> [Extent1].[ProductName])

I'm just going to write code to convert the likes of first to the likes of second if there is nothing better, I'll post the solution here. But maybe it's already done by someone? Or maybe there is a profiler or something, that can give me sql code I can execute partially in SSMS?

like image 439
Andrew Savinykh Avatar asked May 19 '11 06:05

Andrew Savinykh


2 Answers

So here is what I ended up with. A couple of notes:

  • This won't work in 100% of cases, but this is good enough for me
  • There is a lot to improve in terms of usability. Currently I put a shortcut to the compiled binary on the desktop, cut the text to convert to clipboard, double-click the shortcut and paste the result.
using System;
using System.Text.RegularExpressions;
using System.Windows.Forms;

namespace EFC
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            try
            {
                string input = Clipboard.GetText();
                const string header = "exec sp_executesql N'";

                CheckValidInput(input.StartsWith(header), "Input does not start with {0}", header);

                // Find part of the statement that constitutes whatever sp_executesql has to execute
                int bodyStartIndex = header.Length;
                int bodyEndIndex = FindClosingApostroph(input, bodyStartIndex);

                CheckValidInput(bodyEndIndex > 0, "Unable to find closing \"'\" in the body");

                string body = input.Substring(bodyStartIndex, bodyEndIndex - bodyStartIndex);

                // Unescape 's
                body = body.Replace("''", "'");

                // Work out where the paramters are
                int blobEndIndex = FindClosingApostroph(input, bodyEndIndex + 4);
                CheckValidInput(bodyEndIndex > 0, "Unable to find closing \"'\" in the params");

                string ps = input.Substring(blobEndIndex);

                // Reverse, so that P__linq_2 does not get substituted in p__linq_20
                Regex regexEf = new Regex(@"(?<name>@p__linq__(?:\d+))=(?<value>(?:.+?)((?=,@p)|($)))", RegexOptions.RightToLeft);
                Regex regexLinqToSql = new Regex(@"(?<name>@p(?:\d+))=(?<value>(?:.+?)((?=,@p)|($)))", RegexOptions.RightToLeft);

                MatchCollection mcEf = regexEf.Matches(ps);
                MatchCollection mcLinqToSql = regexLinqToSql.Matches(ps);
                MatchCollection mc = mcEf.Count > 0 ? mcEf : mcLinqToSql;

                // substitutes parameters in the statement with their values
                foreach (Match m in mc)
                {
                    string name = m.Groups["name"].Value;
                    string value = m.Groups["value"].Value;
                    body = body.Replace(name, value);
                }

                Clipboard.SetText(body);
                MessageBox.Show("Done!", "CEF");

            }
            catch (ApplicationException ex)
            {
                MessageBox.Show(ex.Message, "Error");
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message, "Error");
                MessageBox.Show(ex.StackTrace, "Error");
            }
        }

        static int FindClosingApostroph(string input, int bodyStartIndex)
        {
            for (int i = bodyStartIndex; i < input.Length; i++)
            {
                if (input[i] == '\'' && i + 1 < input.Length)
                {
                    if (input[i + 1] != '\'')
                    {
                        return i;
                    }
                    i++;
                }
            }

            return -1;
        }

        static void CheckValidInput(bool isValid, string message, params object[] args)
        {
            if (!isValid)
            {
                throw new ApplicationException(string.Format(message, args));
            }
        }
    }
}
like image 150
Andrew Savinykh Avatar answered Nov 14 '22 15:11

Andrew Savinykh


Well,may be this will be helpfull. MSVS 2010 has IntelliTrace. Every time when EF make a query there is an ADO.Net Event with a query

Execute Reader "SELECT TOP (1) 
[Extent1].[id] AS [id], 
[Extent1].[Sid] AS [Sid], 
[Extent1].[Queue] AS [Queue], 
[Extent1].[Extension] AS [Extension]
FROM [dbo].[Operators] AS [Extent1]
WHERE [Extent1].[Sid] = @p__linq__0"    Command Text = "SELECT TOP (1) \r\n[Extent1].[id] AS [id], \r\n[Extent1].[Sid] AS [Sid], \r\n[Extent1].[Queue] AS [Queue], \r\n[Extent1].[Extension] AS [Extension]\r\nFROM [dbo].[Operators] AS [Extent1]\r\nWHERE [Extent1].[Sid] = @p__linq__0", 

Connection String = "Data Source=paris;Initial Catalog=telephony;Integrated Security=True;MultipleActiveResultSets=True"    
like image 32
Yuriy Vikulov Avatar answered Nov 14 '22 17:11

Yuriy Vikulov