65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WCF 和 DbProviderFactory 注入的 DbProvider Profiler:IV 的第 II 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (4投票s)

2011年9月9日

CPOL

5分钟阅读

viewsIcon

23405

downloadIcon

301

在我们的 ProxyDbxxx 类中创建分析信息。

引言

在专业项目中,我们使用 Entity Framework 作为应用程序的 ORM 提供程序。在许多情况下,我们想知道某个 LINQ 查询生成了什么 SQL 查询。这通常发生在某个特定查询运行缓慢、返回意外结果等情况下。为了“调试” LINQ 查询,我们可以使用 VS 2010(Ultimate 版)的强大新功能 IntelliTrace。通过此工具窗口,我们可以检查在给定时间点运行了哪个查询。如果我们有 VS2010 的 Ultimate 许可证,这会很棒,但可惜我们没有。

IntelliTrace 的第二个缺点是它没有告诉我们执行查询的性能。它运行花了 500 毫秒,还是 5 毫秒?我们无从得知。

等一下!为什么不使用 SQL Profiler 软件呢?嗯,我建议你们都尝试使用 SQL-Profiler 来调试 EF 查询。这简直是令人讨厌的(pain in the ****)。

因此,我决定自己寻找一个解决方案,我尽量保持它尽可能基础和简单,所以不要在这里寻找过度工程!

这将是一系列文章,我会尝试在这里维护一个索引,以便您更容易找到不同的部分。

目录

创建代理类

首先,如果我们想真正分析任何东西,我们需要更多信息。因此,以下类将被包装在代理中

  • DbCommand
  • DbConnection
  • DbDataAdapter
  • DbCommandDefinition

这些类,特别是 DbConnectionDbCommand,将为我们提供足够的信息来开始分析发送到 SQL 后端的操作。我们始终会将在我们的包装类中保留对被代理对象的引用,以便我们可以将方法调用、属性 getter/setter 转发到原始实现。我们只会添加一些仪器来测量执行某事所花费的时间。我们还将为每个新的 DbConnection 分配一个 GUID。这是“转发”或代理机制如何工作的示例

这是 ProxyDbConnection 的构造函数的样子

public class ProxyDbConnection : DbConnection
{
    public DbProviderFactory ProxiedFactory { get; private set; }
    public DbConnection ProxiedConnection { get; private set; }
    public string Guid { get; private set; }

    public ProxyDbConnection(DbConnection proxiedConnection, 
           DbProviderFactory proxiedFactory)
    {
        ProxiedConnection = proxiedConnection;
        ProxiedFactory = proxiedFactory;
        Guid = System.Guid.NewGuid().ToString();
    } 
    ...
}

对于属性

public override ConnectionState State
{
    get { return ProxiedConnection.State; }
}

对于方法

public override void Open()
{
    ProxiedConnection.Open();
}

下面您可以看到这些代理类的类图

Client.Proxy.png - Click to enlarge image

初始分析“库”

我创建了一个包含三个方法的接口:IProfilerOutput

Profiler.png

此接口的目的是允许不同的媒体输出我们的分析统计信息。此接口的第一个实现会将所有分析信息定向到 Visual Studio 的 Output 工具窗口。这通过使用 Debug.Write(Line) 方法非常简单地完成。在我继续之前,我应该注意的是,我已经创建了用于通过 WCF 服务传播分析信息所需的 WCF 合同。我在 OutputWindow 实现中使用了这些 DataContracts 来使用 ToString() 方法。

供您参考,这是 WCF 数据合同的类图

Van.Parys.Data.Common.png - Click to enlarge image

这是 DebugProfileOutput 类的完整实现

public class DebugProfileOutput: IProfileOutput
{
    #region Implementation of IProfileOutput
    public void CommandExecute(ProxyDbCommand command, TimeSpan duration, 
           ExecuteMethod method, List<QueryExecutionPlan> executionPlan, 
           object result)
    {
        var commandMessage = new Van.Parys.Data.Common.CommandMessage(
            command.CommandText, duration, method.ToString(), 
            command.ProxyDbConnection.Guid) 
              {QueryExecutionPlans = executionPlan, NonReaderResult = result};
        Debug.WriteLine(commandMessage.ToString());
    }
    public void ContextAttached(string contextName)
    {
        Debug.WriteLine(string.Format("{0} Attached!", contextName));
    }
    public void ConnectionChanged(ProxyDbConnection connection)
    {
        var connectionMessage = new ConnectionMessage() {
            ConnectionGUID = connection.Guid, 
            Open = connection.State == ConnectionState.Open};
        Debug.WriteLine(connectionMessage.ToString());
    }
    #endregion
}

为了测量某个操作花费的时间,我使用了以下非常有用的实用方法

public static class ProfilerStopwatchAction
{
    public static TimeSpan Measure(Action action)
    {
        var startNew = Stopwatch.StartNew();
        //perform action to measure time
        action();
        startNew.Stop();
        return TimeSpan.FromMilliseconds(startNew.ElapsedMilliseconds);
    }
}

用法

... 
TimeSpan duration = ProfilerStopwatchAction.Measure(() => 1 + 1);
... 

分析会话的设置和配置

还记得初始化我们的分析库所需的初始代码吗?猜不着了吧!嗯,它是这样的

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
    //First and only entry point of the profiler
    ProxyGenerator.Init();
            
    Application.Run(new Form1());  
}

嗯,事情将变得复杂两倍!这是因为我向 Init() 函数添加了一个可选参数

public static void Init(ProfilingOptions options = null) 

ProfilingOptions 是一个定义以下选项的类

  • bool IncludeQueryStatistics
    • 这使我们能够将查询执行计划包含在分析输出中。我们都知道在 SQL Management Studio 中获得的图形化输出,嗯,这就是文本表示。
    • 请注意:这不适用于所有 SQL 提供程序!此功能使用“SET SHOWPLAN_ALL ON”命令,该命令对例如 SqlCe 无效!
  • bool ProfileConnections
    • 指示我们是否关心分析连接状态(打开/关闭)
  • ProfilingOutput ProfilingOutput
    • ProfilingOutput 是一个 Enum,它告诉我们使用哪个 IProfileOutput 实现。目前,这里唯一的选项是“Debug”
    public enum ProfilingOutput
    {
        Debug
    } 

所以,现在我们可以对哪些内容进行分析,哪些不进行分析,有更多的控制权。这些选项中值得关注的是包含查询执行计划的能力。该计划的检索是通过以下代码实现的

...
private const string SetShowplanAllOn = "SET SHOWPLAN_ALL ON";
private const string SetShowplanAllOff = "SET SHOWPLAN_ALL OFF";
...
public override object ExecuteScalar()
{
    var executionPlan = ProfileQueryExecutionPlan();

    object executeScalar = null;
    var timeSpan = ProfilerStopwatchAction.Measure(() => 
		executeScalar = ProxiedCommand.ExecuteScalar());
    EFProfiler.CommandExecute(this, timeSpan, ExecuteMethod.ExecuteScalar, 
                              executionPlan, executeScalar);
    return executeScalar;
}
...
internal List<QueryExecutionPlan> ProfileQueryExecutionPlan()
{
    if (!ProxyGenerator.Options.IncludeQueryStatistics) 
        return null;

    var executionPlan = new List<QueryExecutionPlan>();
    if (!profilingQuery)
    {
        profilingQuery = true;
        var cmd = ProxyDbConnection.CreateCommand() as ProxyDbCommand;
        cmd.Connection = this.ProxyDbConnection;
        cmd.CommandText = SetShowplanAllOn;
        cmd.CommandType = CommandType.Text;
        cmd.ProxiedCommand.ExecuteNonQuery();
        cmd.CommandText = this.CommandText;
        cmd.CommandType = CommandType.Text;
        var statsReader = cmd.ProxiedCommand.ExecuteReader();
        while (statsReader.Read())
        {
            executionPlan.Add(ParseReader(statsReader));
        }
        statsReader.Close();
        cmd.CommandText = SetShowplanAllOff;
        cmd.CommandType = CommandType.Text;
        cmd.ProxiedCommand.ExecuteNonQuery();
        profilingQuery = false;
    }
    return executionPlan;
}

在此特定场景中,一个陷阱是避免在 IncludeQueryStatistics 设置为 true 时出现无限循环。发生这种无限循环是因为 ProfileQueryExecutionPlan 方法本身执行三个查询。因此,我们必须避免对这三个查询也进行分析。正如您在上面的示例中所见,这是通过 bool profilingQuery 实现的。

分析 ProxyDbCommand

最后,我们可以开始分析一些东西了!演示应用程序包含在源文件中。这是测试代码

static void Main(string[] args)
{
    //single point of 'intrusion'
    ProxyGenerator.Init(new ProfilingOptions() {IncludeQueryStatistics = true, 
                        ProfilingOutput = ProfilingOutput.Debug});

    Debug.WriteLine("Test Count method");
    var count = new DemoDataEntities().Person.Count();

    Debug.WriteLine("Test Where Take(1)");
    var queryable = new DemoDataEntities().Person.Where(
        x => x.Name.StartsWith("P")).Take(1).ToList();
}

这是输出窗口的快照

Test Count method
a5454996-dd65-4537-97c6-55a0be329837 changed to Open
ExecuteDbDataReader: [1ms] [result:'-1'] : 
SELECT 
[GroupBy1].[A1] AS [C1]
FROM ( SELECT 
       COUNT(1) AS [A1]
       FROM [Person] AS [Extent1]
) AS [GroupBy1]
a5454996-dd65-4537-97c6-55a0be329837 changed to Closed
Test Where Take(1)
5247999a-7d94-43fb-82bd-7e0765244405 changed to Open
ExecuteDbDataReader: [0ms] [result:'-1'] : 
SELECT TOP (1) 
[Extent1].[Id] AS [Id], 
[Extent1].[Name] AS [Name], 
[Extent1].[Function] AS [Function], 
[Extent1].[Description] AS [Description]
FROM [Person] AS [Extent1]
WHERE (CHARINDEX(N'P', [Extent1].[Name])) = 1
5247999a-7d94-43fb-82bd-7e0765244405 changed to Closed

结论

在本文中,我们为我们的库添加了一些实际的分析信息。请随意在您自己的 EF 项目等中尝试。在下一篇文章中,我将探讨如何使用 WCF 服务将分析器公开到外部世界。

下一部分

下一篇文章将介绍创建一个 WCF 客户端/服务器设置,以使外部侦听器能够处理分析信息。

© . All rights reserved.