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

SqlCommand 到 T-SQL 打印机

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.55/5 (8投票s)

2018年3月16日

CPOL

3分钟阅读

viewsIcon

15090

downloadIcon

172

一个 Entity Framework 日志打印机, 可将 SQL 命令转换为可执行的 T-SQL 语句。

引言

在本文中,我们将看到一个与 Entity Framework 协同工作的日志打印机,它将 SqlCommand 转换为一个完全可执行的 T-SQL 语句,包括所有的参数声明,并写入日志文件。一旦命令执行,您将在日志文件中看到如下消息。

-- SQL executed in 1 ms:
DECLARE @p__linq__0  VarChar(100)  = 'Cloudie';
SELECT 
[Extent1].[StudentID] AS [StudentID], 
[Extent1].[StudentName] AS [StudentName], 
FROM [dbo].[Student] AS [Extent1]
WHERE [Extent1].[StudentName] = @p__linq__0
-- Result: SqlDataReader

日志消息还会显示命令所花费的时间。这可以帮助我们调试和测试由 DbContext 执行的每个 SQL 命令。

本文中的示例基于 .NET Framework 4.7 和 Entity Framework 6.2.0。

创建一个拦截器

首先要做的是创建一个实现 IDbCommandInterceptor 接口的类。在本文中,我们将其命名为 SqlLogInterceptor

public class SqlLogInterceptor : IDbCommandInterceptor
{
    #region Fields

    private static readonly TraceSource _traceSource = 
       new TraceSource(nameof(SqlLogInterceptor));

    #endregion

    #region Constructor

    public SqlLogInterceptor() { }

    #endregion

    #region Implementations

    public void NonQueryExecuting(DbCommand cmd, 
        DbCommandInterceptionContext<int> interceptionContext)
    {
        interceptionContext.SetUserState(nameof(Stopwatch), Stopwatch.StartNew()); 
    }

    public void NonQueryExecuted(DbCommand cmd, 
        DbCommandInterceptionContext<int> interceptionContext)
    { 
        var stopwatch = interceptionContext.FindUserState(nameof(Stopwatch)) as Stopwatch;

        // TODO: print T-SQL statement.
    }

    public void ReaderExecuting(DbCommand cmd, 
        DbCommandInterceptionContext<DbDataReader> interceptionContext)
    { 
        interceptionContext.SetUserState(nameof(Stopwatch), Stopwatch.StartNew());
    }

    public void ReaderExecuted(DbCommand cmd, 
        DbCommandInterceptionContext<DbDataReader> interceptionContext)
    { 
        var stopwatch = interceptionContext.FindUserState(nameof(Stopwatch)) as Stopwatch;

        // TODO: print T-SQL statement.
    }

    public void ScalarExecuting(DbCommand cmd, 
        DbCommandInterceptionContext<object> interceptionContext)
    { 
        interceptionContext.SetUserState(nameof(Stopwatch), Stopwatch.StartNew());
    }

    public void ScalarExecuted(DbCommand cmd, 
        DbCommandInterceptionContext<object> interceptionContext)
    { 
        var stopwatch = interceptionContext.FindUserState(nameof(Stopwatch)) as Stopwatch;

        // TODO: print T-SQL statement.
    }

    #endregion
} 

从这些方法的命名中,您已经可以知道它们的用途和执行顺序。例如,NonQueryExecuting() 将在非查询命令即将运行时被调用。而 NonQueryExecuted() 方法将在命令完成后被调用。在每个 -ing 方法中,我们初始化一个 Stopwatch 实例并将其分配给 interceptionContext,然后在相应的 -ed 方法中检索它。这使我们能够测量命令所花费的时间,并将其包含到日志消息中。

要启用拦截器,我们需要在 Web.config 中的 <entityFramework> 节中添加一段配置,如下面的代码片段所示

<entityFramework> 
    <interceptors>
      <interceptor type="LogExample.Logging.SqlLogInterceptor, LogExample"></interceptor>
    </interceptors>
  </entityFramework>

请注意,我们有一个 TraceSource 字段,用于接收日志消息。您可以将其替换为您想要使用的任何东西。

格式化基本类型参数

接下来要做的是处理 SqlCommand 实例使用的每个参数,并将其格式化为 T-SQL 语句。为了实现这一点,我们需要弄清楚它的 SqlDbType,并相应地选择正确的 string 格式。

private static string FormatParameters(DbParameterCollection parameters)
{
    return string.Join(Environment.NewLine, parameters.OfType<SqlParameter>().Select(FormatParameter));
}

private static string FormatParameter(SqlParameter param)
{
    switch (param.SqlDbType)
    {
        case SqlDbType.BigInt:
        case SqlDbType.Binary:
        case SqlDbType.Float:
        case SqlDbType.Int:
        case SqlDbType.TinyInt:
        case SqlDbType.SmallInt:
        case SqlDbType.DateTimeOffset:
        case SqlDbType.Timestamp:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.SqlDbType} {FormatNumericParameter(param)};";
        case SqlDbType.Decimal:
        case SqlDbType.Money:
        case SqlDbType.SmallMoney:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.SqlDbType} {FormatNumericParameterSize(param)} {FormatNumericParameter(param)};";
        case SqlDbType.Bit:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.SqlDbType} {FormatBooleanParameter(param)};";
        case SqlDbType.NText:
        case SqlDbType.Text:
        case SqlDbType.Time:
        case SqlDbType.Xml:
        case SqlDbType.UniqueIdentifier:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.SqlDbType} {FormatStringParameter(param)};";
        case SqlDbType.Char:
        case SqlDbType.NChar:
        case SqlDbType.VarChar:
        case SqlDbType.NVarChar:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.SqlDbType} {FormatStringParameterSize(param)} {FormatStringParameter(param)};";
        case SqlDbType.Date:
        case SqlDbType.DateTime:
        case SqlDbType.DateTime2:
        case SqlDbType.SmallDateTime:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.SqlDbType} {FormatDateTimeParameter(param)};";
        case SqlDbType.Structured:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.TypeName} {FormatDataTableParameter(param)};";
        case SqlDbType.Udt:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.UdtTypeName} -- {param.Value};";
        default:
            return $"DECLARE @{param.ParameterName.TrimStart('@')} {param.SqlDbType} {FormatNumericParameter(param)};";
    }
}

private static string FormatNumericParameter(SqlParameter param)
{
    if (param.Direction == ParameterDirection.Input)
        return " = " + (Convert.IsDBNull(param.Value) ? "NULL" : Convert.ToString(param.Value));
    else
        return string.Empty;
}


private static string FormatNumericParameterSize(SqlParameter param)
{
    if (param.Direction == ParameterDirection.Input || !Convert.IsDBNull(param.Value))
        return string.Empty;

    return $"({param.Precision}, {param.Scale})";
}

private static string FormatBooleanParameter(SqlParameter param)
{
    if (param.Direction == ParameterDirection.Input)
        return " = " + (Convert.IsDBNull(param.Value) ? 
                   "NULL" : Convert.ToString(Convert.ToByte(param.Value)));
    else
        return string.Empty;
}

private static string FormatStringParameter(SqlParameter param)
{
    if (param.Direction == ParameterDirection.Input)
        return " = " + (Convert.IsDBNull(param.Value) ? "NULL" : $"'{param.Value}'");
    else
        return string.Empty;
}

private static string FormatStringParameterSize(SqlParameter param)
{
    if (Convert.IsDBNull(param.SqlValue)) return string.Empty;
    if (param.Size < 0) return "(MAX)";
    if (param.Size > 0) return $"({param.Size})";

    return $"({Convert.ToString(param.SqlValue).Length})";
}

private static string FormatDateTimeParameter(SqlParameter param)
{
    if (param.Direction == ParameterDirection.Input)
        return " = " + (Convert.IsDBNull(param.Value) ? 
                        "NULL" : $"'{param.Value:yyyy-MM-ddTHH:mm:ss}'");
    else
        return string.Empty;
}

这将为日志消息中的每个 SqlCommand 实例生成一个 DECLARE 语句。下面显示了一些示例

DECLARE @QueryString VarChar(200)  = 'string';
DECLARE @BeginDate DateTime  = '1970-01-01T08:00:00';
DECLARE @PageNumber Int  = 0;

格式化结构化类型参数

结构化类型参数处理起来有点复杂。结构化类型在 SQL Server 中定义,通常用于将数组传递给存储过程。在应用程序端,我们需要创建一个 DataTable 实例,并向其添加一个或多个 DataRow 实例。在 T-SQL 中,该语句由一个 DECLARE 语句和一个或多个 INSERT 语句组成。为了打印结构化类型参数,我们需要获取所有列和行,然后根据每个单元格的数据类型选择正确的格式。在本文中,我们使用 GetTypeCode 方法。

private static string FormatDataTableParameter(SqlParameter param)
{
    if (param.Direction == ParameterDirection.Input)
    {
        var table = param.Value as DataTable;
        if (table.Rows.Count == 0) return string.Empty;

        var columns = table.Columns.OfType<DataColumn>().ToArray();
        var rows = table.Rows.OfType<DataRow>().Select
                   (row => $"({string.Join(", ", FormatDataRowColumns(columns, row))})");
        var sb = new StringBuilder(Environment.NewLine);

        sb.AppendLine($"INSERT INTO @{param.ParameterName.TrimStart('@')} 
                ({string.Join(", ", columns.Select(col => col.ColumnName))}) VALUES");
        sb.Append(string.Join("," + Environment.NewLine, rows));

        return sb.ToString();
    }
    return string.Empty;
}

private static IEnumerable<string> FormatDataRowColumns(DataColumn[] columns, DataRow row)
{
    foreach (var col in columns)
    {
        var value = row[col];

        switch (Type.GetTypeCode(col.DataType))
        {
            case TypeCode.Empty:
                yield return string.Empty;
                break;
            case TypeCode.Object:
                yield return $"{row[col]}";
                break;
            case TypeCode.DBNull:
                yield return $"NULL";
                break;
            case TypeCode.Boolean:
                yield return $"{(Convert.ToBoolean(row[col]) ? 1 : 0)}";
                break;
            case TypeCode.Char:
            case TypeCode.String:
                yield return $"N'{row[col]}'"; // TODO: Add your logic to remove prefix N
                break;
            case TypeCode.SByte:
            case TypeCode.Byte:
            case TypeCode.Int16:
            case TypeCode.UInt16:
            case TypeCode.Int32:
            case TypeCode.UInt32:
            case TypeCode.Int64:
            case TypeCode.UInt64:
            case TypeCode.Single:
            case TypeCode.Double:
            case TypeCode.Decimal:
                yield return $"{row[col]}";
                break;
            case TypeCode.DateTime:
                yield return $"'{row[col]:yyyy-MM-ddTHH:mm:ss}'";
                break;
            default:
                yield return string.Empty;
                break;
        }
    }
}

上面的代码片段将产生以下语句

DECLARE @YourParamter dbo.YourStructuredType 
INSERT INTO @YourParamter (Column1Name, Column2Name)  VALUES
(10001001, N'Robert'),
(20002001, N'Cloudie'),
(30003001, N'Rachel');

生成完整的 SQL 语句

SqlLogInterceptor 中,现在我们可以在每个 -ed 方法中生成完整的 SQL 语句以及执行时间、异常和结果,并写入日志消息。除了日志消息,我们还会检查 Exception 属性来确定消息级别。

public void ScalarExecuted(DbCommand cmd, 
        DbCommandInterceptionContext<object> interceptionContext)
{ 
    var stopwatch = interceptionContext.FindUserState(nameof(Stopwatch)) as Stopwatch;
    var type = interceptionContext.Exception == null ? 
               TraceEventType.Information : TraceEventType.Error;

    _traceSource.TraceEvent(type, 1,
        string.Join(Environment.NewLine,
            $"-- SQL executed in {stopwatch.ElapsedMilliseconds} ms:",
            $"USE {cmd.Connection.Database};",
            FormatParameters(cmd.Parameters),
            $"{cmd.CommandText};",
            interceptionContext.Exception,
            $"-- Result: {interceptionContext.Result}"));
}

// Same stuff for other -ed methods

上面的代码片段将生成以下日志消息。

-- SQL executed in 1 ms:
DECLARE @p__linq__0  VarChar(200)  = 'Robert';
SELECT 
[Extent1].[StudentID] AS [StudentID], 
[Extent1].[StudentName] AS [StudentName], 
[Extent1].[StandardId] AS [StandardId]
FROM [dbo].[Student] AS [Extent1]
WHERE [Extent1].[StudentName] = @p__linq__0
-- Result: SqlDataReader

如果执行目标是存储过程,则日志消息将如下所示

-- SQL executed in 20 ms:
DECLARE @QueryString VarChar(200)  = 'string';
DECLARE @BeginDate DateTime  = '1970-01-01T08:00:00';
DECLARE @PageNumber Int  = 0;
EXEC [dbo].[YourStoredProcedure] @QueryString, @BeginDate, @PageNumber;
-- Result: 1

结论

Entity Framework 提供了一种可访问的方式来监视每个命令。本文中的代码利用了它,并在日志文件中生成可执行的 T-SQL 语句。我们可以直接复制它们,并在 SQL Server Management Studio 上调试它们,从而节省我们的调试时间。

历史

  • 2018-03-16 首次发布
  • 2018-03-22 修复了一个错误,即 CHAR, VARCHAR, NCHAR, VARCHAR 数据大小未出现在 DECLARE 语句中。
  • 2018-03-23 修复了一个错误,即 CHAR, VARCHAR, NCHAR, VARCHAR 数据大小未正确显示。向 DECIMAL, MONEYSMALLMONEY 声明添加精度和小数位数。
© . All rights reserved.