EntityFramework之Log(五)

关于日志

属性日志

  DbContext.Database.Log 属性被设置为一个委托,该委托能接受带有一个字符串参数的任何方法,最主要的是,通过设置它到 TextWriter 的 Write 方法将能应用于任何的TextWriter,通过上下文自动生成的所有SQL语句将被记录到Writer中。

例如,如下代码将记录SQL在控制台上:

              using (var ctx = new EntityDbContext())
              {
                 ctx.Database.Log = Console.WriteLine;
              }

【注意】上下文中的日志被设置到 Console.WriteLine ;则其所有SQL代码都将会输出在控制台上。

下面我们进行一些简单的查询、修改利用日志属性来演示在控制台上进行输出(依然利用前一篇文章所给出个三给类,如若不知其关系,请参考前一篇文章):

            using (var ctx = new EntityDbContext())
            {
                ctx.Database.Log = Console.WriteLine;
或者
ctx.Database.log = s => Console.WriteLine(s);
var stu = ctx.Set<Student>().First(p => p.Name == "xpy0928"); stu.Grades.First().Fraction = 90; stu.Grades.Add(new Grade() { Fraction = 40 }); ctx.SaveChangesAsync().Wait(); }

则在控制台打印如下SQL代码:

 日志记录内容

(1)各种SQL命令

    查询语句。例如:Linq查询、原始SQL查询

    作为SaveChanges的一部分的插入(inserted)、修改(update)以及删除(delete)

    由延迟加载自动生成的关联查询

(2)参数

(3)是否正在被异步执行的命令

(4)当命令开始执行时,时间戳的显示

(5)无论命令是否被成功完成,还是通过抛出异常而失败或者是通过异步被取消

(6)一些显示结果的值

(7)执行命令所需要的时间

输出日志

依上述,将日志输出在控制台是so easy,像这种输出在控制台中只能作为一些小demo,所以应用性不广,如果你要是在window form中或者是Web应用程序中的话,完全可以将日志输出在内存、文件等中。下面演示输出到文件中,其余不予演示。

            using (var ctx = new EntityDbContext())
            {
var sw = new StreamWriter(@"d:\Data.log") { AutoFlush = true };
ctx.Database.Log
= s => { sw.Write(s); }; }

结果文件中输出如下:

那么问题来了,如果我们需要将输出的内容进行格式化,那么我们应该怎样通过一种简单的方式怎来做呢?

稍等,容我一一讲来。

日志结果

默认的日志记录器记录sql语句文本,参数以及在命令发到数据库之前带着时间戳的“Executeing”(通过上述文本可知)。一个“Completed”(完成的)包含总的时间被记录在执行命令的下面。

【注意】异步命令中的“Completed”是直到异步任务执行完成、失败或者被取消才被记录下来,当然,它因不同的命令和是否成功执行而产生不同的信息。

执行成功

成功完成输出的是如上述文件中的 执行-- 已在 0 毫秒内完成,结果为: SqlDataReader 

执行失败

通过异常来告知失败,输出中包含了失败的信息。

执行取消

异步命令中的任务被取消会通过异常来告知结果是失败的。因为当试图去取消时,底层的ADO.NET会这样操作,而EF是基于ADO.NET的。

日志格式化

在Database.log属性下我们要充分使用 DatabaseLogFormatter 对象,这个对象有效结合了 IDbCommandInterceptor 来实现,通过接受一个字符串的委托和上下文。这意味着在DatabaseLogFormatter上截取方法之前和通过EF执行命令之后被调用,这些DatabaseLogFormatter方法获取有格式的日志并将其传递给一个委托代理。

 

通过从DatabaseLogFormatter上派生出一个类并且适当的重写其方法就能实现有格式的日志输出。重写最常用的方法有以下三者:

 LogCommand 

在命令被执行之前重写此方法来进行更改。通过默认的LogCommand来调用LogParameter的每个参数。当然你也可以进行重写或者处理参数不同。

 LogResult 

重写此方法来更改原有执行命令后记录下来的结果。

 LogParameter

重写此方法来更改其格式和参数所记录的内容。

 例如,在每条命令被发送到数据库之前,我们想仅仅通过单行来进行记录。这就需要重写两个方法:

(1)重写 LogCommand 来得到SQL单行的格式

(2)重写 LogResult 不需要做任何动作。(当执行时让其执行可重写的,因为这样在性能上可能会稍微高效一点,但是在功能还是等同的)

依据上述,下面我们来写出代码

    public class SingleLineFormatter : DatabaseLogFormatter
    {
        public SingleLineFormatter(DbContext ctx, Action<string> action)
            : base(ctx, action)
        {

        }

        public override void LogCommand<TResult>(System.Data.Common.DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
        {
            Write(
                string.Format("DbContext '{0}' is Executing Command '{1}' '{2}'",
                Context.GetType().Name,
                command.CommandText.Replace(Environment.NewLine,""),
                Environment.NewLine));
base.LogCommand<TResult>(command, interceptionContext); } public override void LogResult<TResult>(System.Data.Common.DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext) { base.LogResult<TResult>(command, interceptionContext); } }

上述日志输出会调用 Write 方法,此方法将输出发送到配置的写入委托。

【注意】上述代码只是通过一种简单的方式除去换行符,如果是在复杂的SQL语句中,上述方式可能会没有效果。

设置DatabaseLogFormatter

 一旦DatabaseLogFormatter的派生类被创建,那么你需要将其注册到EF中,否则也无法进行格式化输出。通过使用Code-Based Configuration,这也就是说,创建一个新的继承自DbConfiguration的类与DbContext上下文类所在同一个程序集,然后会在创建的新类的构造函数中调用 SetDatabaseLogFormatter 方法,该方法接受的参数就是格式化类中构造函数中的两个参数。

    public class DbContextConfiguration : DbConfiguration
    {
        public DbContextConfiguration()
        {
            SetDatabaseLogFormatter(
                (context, action) => new SingleLineFormatter(context, action));
        }
    }

基于上我们新创建的 SingleLineFormatter 会应用在设置使用Database.log的任何时刻,所以你会看到如下结果:

 

 接下来我们将看 IDbCommandInterceptor 实现直接来控制命令的拦截,并通过集成使用NLog的例子而不使用Database.log。请继续往下看

低级构造块监听

接口监听

监听代码实际上是监听接口的概念。这些接口来继承自 IDbInterceptor 并且当EF有所行为时其定义的方法会被调用。其意义是在每个对象被截获时都有一个接口。例如,当EF调用ExecuteNonQuery, ExecuteScalar, ExecuteReader等相关的方法时在IDbInterceptor上定义的方法将会被调用。同样,当每个操作完成这些方法也会被调用,上述中的DatabaseLogFormatter类就实现了这个接口来记录命令。

接口存在的意义

(1)为了记录SQL命令

(2)为了支持并实现一些其他特性

处理结果

泛型 DbCommandInterceptionContext<> 类里面包含几个属性称之为Result, OriginalResult, Exception, and OriginalException。这些方法会被设置为空通过在操作被执行之前调用的拦截方法。如果操作被成功执行,那么 Result and OriginalResult会被设置成操作的结果。这些值会被在操作执行完后的监听方法所监控。同理,如果操作抛出异常,那么Exception and OriginalException将会被设置。

禁止执行

如果一个监听者在命令快执行完之前设置了Result属性的话,那么EF实际上是不会试图去执行该命令,但是代替的是仅仅是使用结果集。换言之,这个监听者会禁止命令的执行,但是EF会继续,就好像命令已经被执行了。

 

发生上述禁止执行的示例可能是经过包装的批处理命令,这个监听者会将其储存以便日后将用于批处理但是会装到EF中一如往常的执行该命令。注意监听者需要更多这来实现批处理。

执行后改变结果

如果一个监听者在命令执行完后设置了Result属性,那么EF将会使用改变后的结果而不是通过此操作返回实际的结果。同样,如果一个监听者在命令执行完后设置了Exception属性,那么EF将抛出设置的异常,就好像此操作已经抛出了这个异常一样。

 

一个监听者也可以设置Exception属性为空来表明没有异常应该被抛出。这其实是非常有意义的,如果执行操作失败但是监听者希望EF继续运行就好像操作成功执行了一样。

OriginalResult 和OriginalException

在EF执行完一个操作后,如果该操作未失败将设置Result和OriginalResult属性,或者如果执行失败抛出异常那么将设置Exception和OrigianlException。

 

在实际执行完一个操作之后, OriginalResult和OriginalException会被EF设置为只读的。这些属性不会被监听者所设置。这也就意味着,任何监听者能区分Exception(异常)和Result(结果),那些会通过一些其他的相对于操作被执行时发生的真实的异常(Exception)和结果(Result)的监听者所设置。

 注册监听者

 一旦创建了一个实现了一个或者多个监听接口的类需要通过  DbInterception 注册到EF中。例如:

DbInterception.Add(new NLogCommandInterceptor());

监听者也能够使用基于代码的配置机制(Code-Based DbConfiguration )被注册在应用程序域中。

下面我们将基于以上描述通过使用 IDbCommandInterceptor ,来讲述一个实例: Log To NLog (通过NLog进行日志记录)(关于NLog请看这里:NLog Tutorial

记录如下日志:

(1)执行非异步的命令作为Warning(警告)

(2)执行抛出的异常作为Error(严重错误)

    public class NLogCommandInterceptor : IDbCommandInterceptor
    {
        
        private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

        public void NonQueryExecuting(
            DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            LogIfNonAsync(command, interceptionContext);
        }

        public void NonQueryExecuted(
            DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
            LogIfError(command, interceptionContext);
        }

        public void ReaderExecuting(
            DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            LogIfNonAsync(command, interceptionContext);
        }

        public void ReaderExecuted(
            DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            LogIfError(command, interceptionContext);
        }

        public void ScalarExecuting(
            DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            LogIfNonAsync(command, interceptionContext);
        }

        public void ScalarExecuted(
            DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            LogIfError(command, interceptionContext);
        }

        private void LogIfNonAsync<TResult>(
            DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
        {
            if (!interceptionContext.IsAsync)
            {
                Logger.Warn("Non-async command used: {0}", command.CommandText);
            }
        }

        private void LogIfError<TResult>(
            DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
        {
            if (interceptionContext.Exception != null)
            {
                Logger.Error("Command {0} failed with exception {1}",
                    command.CommandText, interceptionContext.Exception);
            }
        }

       
    }

结果如下:

【注意】上述NLog得安装如下程序包

接下来在NLog.config中rules和target节点下进行简单的配置输出日志即可,如下:

  <rules> 
    <logger name="ConsoleApplication1.NLogCommandInterceptor" minlevel="Info" writeTo="f"></logger> 
/*name为命名空间+实现IDbCommandInterceptor接口的监控类名称,f为写到下面target中的name*/ </rules>
  <targets>
    <target name="f" xsi:type="File" fileName="c:\temp\log.txt"/>
  </targets>

监听中的Dispatch方法

除了在 DbInterception 中注册方法外,它也提供了Dispatch方法,此方法允许代码不是分配到监听的通知的一部分,这是以上已经提到的机制,允许提供者让监听者知道在EF的控制下,一条命令正在被执行。不过对于应用程序开发者去使用Dispatch API这是比较少见的。下面了解下这个方法如何去操作。

DbInterception.Dispatch.Command.NonQueryAsync(myCommand, new DbCommandInterceptionContext());

以上代码做了以下五件事:

(1)确保被设置到监听上下文中的命令是否是  IsAsync  的

(2)调用所有注册在 IDbCommandInterceptors 中的 NonQueryExecuting  方法

(3)除非如以上所说这些 NonQueryExecuting 方法之一被设置了Result属性,否则调用 ExecuteNonQueryAsync  

(4)在异步任务中设置了延续,使得注册在 IDbCommandInterceptors 上的所有 NonQueryExecuted 方法都被调用

(5)使得任务结果中包含了可能已经被监听集合中的方法之一改变了的正确结果

打开日志无需重新编译(EF 6.1)

上述我们是通过手动操作来进行日志的输出,如果你嫌麻烦,大可在配置文件(web.config或App.config)中的 EntityFramework 节点下进行相关配置来完成日志输出工作。

(1)输出到控制台

<interceptors>
  <interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework"/>
</interceptors>

(2)输出到文件

<interceptors>
  <interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework">
    <parameters>
      <parameter value="c:\temp\log.txt"/>
    </parameters>
  </interceptor>
</interceptors>

如下结果:

 

【注意】上述默认情况下,当应用程序每次启动会重新生成一个新的log.text,则此前的将被覆盖。如果该文件总是存在的话,为了追加到到日志文件中,可以进行如下操作:

<interceptors>
  <interceptor type="System.Data.Entity.Infrastructure.Interception.DatabaseLogger, EntityFramework">
    <parameters>
      <parameter value="c:\temp\log.txt"/>
      <parameter value="true" type="System.Boolean"/>
    </parameters>
  </interceptor>
</interceptors>

上述就是通过注册监听集合下的interceptor(监听者)来进行日志输出。

IDbConfigurationInterceptor

在EF6.1中引入了此接口,它是一个当应用程序启动时允许代码检测或者修改EF configuration的监听接口。通过这个我们能够实现一个关于 DatabaseLogger 的简单版本

public class ExampleDatabaseLogger : IDbConfigurationInterceptor
{
    public void Loaded(
        DbConfigurationLoadedEventArgs loadedEventArgs,
        DbConfigurationInterceptionContext interceptionContext)
    {
        var formatterFactory = loadedEventArgs
                .DependencyResolver
                .GetService<Func<DbContext, Action<string>, DatabaseLogFormatter>>();
 
        var formatter = formatterFactory(null, Console.Write);
 
        DbInterception.Add(formatter);
    }
}

我们来分析下上述代码:

(1)第一行要求EF去注册 DatabaseLogFormatter 工厂,上述我们只是新建了一个  DatabaseLogFormatter 来代替,当然如果应用程序在此行自定义了格式化 ,那么将通过用此自定义的而不会是默认的

(2)第一行实际上是没有获得DatabaseLogFormatter,它只是获得一个来创建DatabaseLogFormatter实例的一个工厂,第二行调用工厂来获得一个实际意义上的formatter实例。因为我们要记录所有上下文实例,所以为空也是允许的。工厂的第二个参数是对于控制台输出流的委托,因此我们只需将指针指向Console.Write()方法即可。同理如果我们要将日志记录到文件中,则需要通过StreanWrite的实例来实现,上述已经演示。

(3)第三行将DatabaseLogFormatter作为一个监听者来进行注册,在EF中如何就监听者来进行日志记录,上述已经描述。

 

posted @ 2015-08-26 15:30  Jeffcky  阅读(12478)  评论(68编辑  收藏  举报