Share via


Veritabanı işlemlerini günlüğe kaydetme ve kesme

Dekont

Yalnızca EF6'ya Doğru - Bu sayfada ele alınan özellikler, API'ler vb. Entity Framework 6'da sunulmuştur. Önceki bir sürümü kullanıyorsanız, bilgilerin bir kısmı veya tümü geçerli değildir.

Entity Framework 6'dan başlayarak, Entity Framework veritabanına her komut gönderdiğinde bu komut uygulama kodu tarafından kesilebilir. Bu en yaygın olarak SQL'i günlüğe kaydetmek için kullanılır, ancak komutu değiştirmek veya durdurmak için de kullanılabilir.

Özellikle EF şunları içerir:

  • LINQ to SQL'de DataContext.Log'a benzer bir bağlam için Log özelliği
  • Günlüğe gönderilen çıkışın içeriğini ve biçimlendirmesini özelleştirme mekanizması
  • Daha fazla denetim/esneklik sağlayan kesme için düşük düzeyli yapı taşları

Bağlam Günlüğü özelliği

DbContext.Database.Log özelliği, dize alan herhangi bir yöntem için temsilci olarak ayarlanabilir. En yaygın olarak herhangi bir TextWriter ile bu TextWriter'ın "Write" yöntemine ayarlanarak kullanılır. Geçerli bağlam tarafından oluşturulan tüm SQL bu yazıcıya kaydedilir. Örneğin, aşağıdaki kod SQL'i konsolda günlüğe kaydeder:

using (var context = new BlogContext())
{
    context.Database.Log = Console.Write;

    // Your code here...
}

Bu bağlama dikkat edin. Database.Log, Console.Write olarak ayarlanır. SQL'i konsolda günlüğe kaydetmek için gereken tek şey budur.

Bazı çıkışları görebilmek için basit bir sorgu/ekleme/güncelleştirme kodu ekleyelim:

using (var context = new BlogContext())
{
    context.Database.Log = Console.Write;

    var blog = context.Blogs.First(b => b.Title == "One Unicorn");

    blog.Posts.First().Title = "Green Eggs and Ham";

    blog.Posts.Add(new Post { Title = "I do not like them!" });

    context.SaveChanges();
}

Bu, aşağıdaki çıkışı oluşturur:

SELECT TOP (1)
    [Extent1].[Id] AS [Id],
    [Extent1].[Title] AS [Title]
    FROM [dbo].[Blogs] AS [Extent1]
    WHERE (N'One Unicorn' = [Extent1].[Title]) AND ([Extent1].[Title] IS NOT NULL)
-- Executing at 10/8/2013 10:55:41 AM -07:00
-- Completed in 4 ms with result: SqlDataReader

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Title] AS [Title],
    [Extent1].[BlogId] AS [BlogId]
    FROM [dbo].[Posts] AS [Extent1]
    WHERE [Extent1].[BlogId] = @EntityKeyValue1
-- EntityKeyValue1: '1' (Type = Int32)
-- Executing at 10/8/2013 10:55:41 AM -07:00
-- Completed in 2 ms with result: SqlDataReader

UPDATE [dbo].[Posts]
SET [Title] = @0
WHERE ([Id] = @1)
-- @0: 'Green Eggs and Ham' (Type = String, Size = -1)
-- @1: '1' (Type = Int32)
-- Executing asynchronously at 10/8/2013 10:55:41 AM -07:00
-- Completed in 12 ms with result: 1

INSERT [dbo].[Posts]([Title], [BlogId])
VALUES (@0, @1)
SELECT [Id]
FROM [dbo].[Posts]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'I do not like them!' (Type = String, Size = -1)
-- @1: '1' (Type = Int32)
-- Executing asynchronously at 10/8/2013 10:55:41 AM -07:00
-- Completed in 2 ms with result: SqlDataReader

(Bunun, herhangi bir veritabanı başlatma işleminin zaten gerçekleştiği varsayılarak yapılan çıkış olduğunu unutmayın. Veritabanı başlatma işlemi henüz gerçekleşmemiş olsaydı, yeni bir veritabanını denetlemek veya oluşturmak için Geçişler'in kapaklar altında yaptığı tüm çalışmaları gösteren çok daha fazla çıkış olacaktır.)

Günlüğe ne kaydedilir?

Log özelliği ayarlandığında aşağıdakilerin tümü günlüğe kaydedilir:

  • Tüm farklı komut türleri için SQL. Örnek:
    • Normal LINQ sorguları, eSQL sorguları ve SqlQuery gibi yöntemlerden ham sorgular da dahil olmak üzere sorgular
    • SaveChanges'in bir parçası olarak oluşturulan eklemeler, güncelleştirmeler ve silmeler
    • Gecikmeli yükleme tarafından oluşturulanlar gibi ilişki yükleme sorguları
  • Parametreler
  • Komutun zaman uyumsuz olarak yürütülüyor olup olmadığı
  • Komutun ne zaman yürütülürken başlatıldığını gösteren zaman damgası
  • Komutun başarıyla tamamlanıp tamamlanmadığı, özel durum oluşturularak başarısız olup olmadığı veya zaman uyumsuz olarak iptal edilip edilmediği
  • Sonuç değerinin bazı göstergeleri
  • Komutu yürütmek için geçen yaklaşık süre. Komutu göndermekten sonuç nesnesini geri almaya kadar olan zamanın geldiğini unutmayın. Sonuçları okumak için zaman içermez.

Yukarıdaki örnek çıkışa baktığımızda günlüğe kaydedilen dört komutun her biri şunlardır:

  • Bağlama yapılan çağrıdan kaynaklanan sorgu. Bloglar.First
    • "İlk" toString'in çağrılabileceği bir IQueryable sağlamadığından, SQL'i almak için ToString yönteminin bu sorgu için çalışmadığını fark edin
  • Blogun yavaş yüklenmesinden kaynaklanan sorgu. Mesaj
    • Gecikmeli yüklemenin gerçekleştiği anahtar değeri için parametre ayrıntılarına dikkat edin
    • Yalnızca varsayılan olmayan değerlere ayarlanmış parametrenin özellikleri günlüğe kaydedilir. Örneğin, Size özelliği yalnızca sıfır olmayan bir özellikse gösterilir.
  • SaveChangesAsync'ten kaynaklanan iki komut; bir gönderi başlığını değiştirme güncelleştirmesi için, diğeri eklemenin yeni gönderi eklemesi için
    • FK ve Title özellikleri için parametre ayrıntılarına dikkat edin
    • Bu komutların zaman uyumsuz olarak yürütüldüğünü fark edin

Farklı yerlerde günlüğe kaydetme

Yukarıda gösterildiği gibi konsola günlük kaydı yapmak çok kolaydır. Ayrıca farklı türlerde TextWriter kullanarak belleğe, dosyaya vb. kolayca oturum açabilirsiniz.

LINQ to SQL hakkında bilgi sahibiyseniz, SQL'e LINQ'de Log özelliğinin gerçek TextWriter nesnesine (örneğin, Console.Out) ayarlandığını, EF'de ise Log özelliğinin dize kabul eden bir yönteme ayarlandığını fark edebilirsiniz (örneğin, Console.Write veya Console.Out.Write). Bunun nedeni, dizeler için havuz görevi yapabilecek herhangi bir temsilciyi kabul ederek EF'yi TextWriter'dan ayırmadır. Örneğin, zaten bazı günlüğe kaydetme çerçeveniz olduğunu ve bunun gibi bir günlüğe kaydetme yöntemi tanımladığını düşünün:

public class MyLogger
{
    public void Log(string component, string message)
    {
        Console.WriteLine("Component: {0} Message: {1} ", component, message);
    }
}

Bu, EF Log özelliğine şu şekilde bağlanabilir:

var logger = new MyLogger();
context.Database.Log = s => logger.Log("EFApp", s);

Sonuç günlüğü

Varsayılan günlükçü, komut veritabanına gönderilmeden önce komut metnini (SQL), parametreleri ve "Yürütülüyor" satırını zaman damgasıyla günlüğe kaydeder. Komutun yürütülmesinden sonra geçen süreyi içeren bir "tamamlandı" satırı günlüğe kaydedilir.

Zaman uyumsuz komutlar için, zaman uyumsuz görev gerçekten tamamlanana, başarısız olana veya iptal edilene kadar "tamamlandı" satırının günlüğe kaydedilmediğini unutmayın.

"Tamamlandı" satırı, komutun türüne ve yürütmenin başarılı olup olmadığına bağlı olarak farklı bilgiler içerir.

Başarılı yürütme

Başarıyla tamamlanan komutlar için çıkış "X ms ile tamamlandı sonucu: " şeklindedir ve ardından sonucun ne olduğuna ilişkin bazı göstergeler gösterilir. Veri okuyucu döndüren komutlar için sonuç göstergesi, döndürülen DbDataReader türüdür. Yukarıda gösterilen güncelleştirme komutu gibi bir tamsayı değeri döndüren komutlar için gösterilen sonuç bu tamsayıdır.

Başarısız yürütme

Özel durum oluşturarak başarısız olan komutlar için çıkış, özel durumdan gelen iletiyi içerir. Örneğin, SqlQuery'nin mevcut olan bir tabloya karşı sorgu yapmak için kullanılması, günlük çıkışının şuna benzer bir sonuç vermesine neden olur:

SELECT * from ThisTableIsMissing
-- Executing at 5/13/2013 10:19:05 AM
-- Failed in 1 ms with error: Invalid object name 'ThisTableIsMissing'.

Yürütme iptal edildi

Görevin iptal edildiği zaman uyumsuz komutlar için, temel alınan ADO.NET sağlayıcısının iptal girişiminde bulunduğunda genellikle yaptığı şey bu olduğundan, sonuç bir özel durumla başarısız olabilir. Bu gerçekleşmezse ve görev temiz bir şekilde iptal edilirse çıkış şuna benzer olacaktır:

update Blogs set Title = 'No' where Id = -1
-- Executing asynchronously at 5/13/2013 10:21:10 AM
-- Canceled in 1 ms

Günlük içeriğini ve biçimlendirmesini değiştirme

Database.Log özelliği, databaselogformatter nesnesinin kullanımını kapsar. Bu nesne, bir IDbCommandInterceptor uygulamasını (aşağıya bakın) dizeleri ve DbContext'i kabul eden bir temsilciye etkili bir şekilde bağlar. Bu, DatabaseLogFormatter üzerindeki yöntemlerin EF tarafından komutların yürütülmesinden önce ve sonra çağrıldığı anlamına gelir. Bu DatabaseLogFormatter yöntemleri günlük çıkışını toplar ve biçimlendirip temsilciye gönderir.

DatabaseLogFormatter'i Özelleştirme

Günlüğe kaydedilenleri ve nasıl biçimlendirildiğini değiştirmek, DatabaseLogFormatter'dan türetilen ve yöntemleri uygun şekilde geçersiz kılan yeni bir sınıf oluşturularak gerçekleştirilebilir. Geçersiz kılmak için en yaygın yöntemler şunlardır:

  • LogCommand : Komutların yürütülmeden önce günlüğe kaydedilme biçimini değiştirmek için bunu geçersiz kılın. Varsayılan olarak LogCommand her parametre için LogParameter'i çağırır; bunun yerine geçersiz kılma işleminizde de aynı işlemi yapmayı veya parametreleri farklı şekilde işlemeyi seçebilirsiniz.
  • LogResult : Komutun yürütülmesinden elde edilen sonucun günlüğe kaydedilme biçimini değiştirmek için bunu geçersiz kılın.
  • LogParameter : Parametre günlüğünün biçimlendirmesini ve içeriğini değiştirmek için bunu geçersiz kılın.

Örneğin, her komut veritabanına gönderilmeden önce yalnızca tek bir satır günlüğe kaydetmek istediğimizi varsayalım. Bu işlem iki geçersiz kılmayla yapılabilir:

  • SQL'in tek satırını biçimlendirmek ve yazmak için LogCommand'ı geçersiz kılma
  • Hiçbir şey yapmak için LogResult'ı geçersiz kılın.

Kod şuna benzer olacaktır:

public class OneLineFormatter : DatabaseLogFormatter
{
    public OneLineFormatter(DbContext context, Action<string> writeAction)
        : base(context, writeAction)
    {
    }

    public override void LogCommand<TResult>(
        DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
    {
        Write(string.Format(
            "Context '{0}' is executing command '{1}'{2}",
            Context.GetType().Name,
            command.CommandText.Replace(Environment.NewLine, ""),
            Environment.NewLine));
    }

    public override void LogResult<TResult>(
        DbCommand command, DbCommandInterceptionContext<TResult> interceptionContext)
    {
    }
}

Çıkışı günlüğe kaydetmek için, çıktıyı yapılandırılmış yazma temsilcisine gönderecek olan Write yöntemini çağırmanız yeterlidir.

(Bu kodun, örnek olarak satır sonlarını basit bir şekilde kaldırdığını unutmayın. Karmaşık SQL'i görüntülemek için iyi çalışmayacaktır.)

DatabaseLogFormatter'i ayarlama

Yeni bir DatabaseLogFormatter sınıfı oluşturulduktan sonra EF'ye kaydedilmesi gerekir. Bu işlem kod tabanlı yapılandırma kullanılarak yapılır. Özetle bu, DbContext sınıfınızla aynı derlemede DbConfiguration'dan türetilen yeni bir sınıf oluşturmak ve ardından bu yeni sınıfın oluşturucusunda SetDatabaseLogFormatter'ı çağırmak anlamına gelir. Örnek:

public class MyDbConfiguration : DbConfiguration
{
    public MyDbConfiguration()
    {
        SetDatabaseLogFormatter(
            (context, writeAction) => new OneLineFormatter(context, writeAction));
    }
}

Yeni DatabaseLogFormatter'i kullanma

Bu yeni DatabaseLogFormatter artık Database.Log ayarlandığında kullanılacak. Bu nedenle, 1. bölümden kodu çalıştırmak artık aşağıdaki çıkışla sonuçlanır:

Context 'BlogContext' is executing command 'SELECT TOP (1) [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title]FROM [dbo].[Blogs] AS [Extent1]WHERE (N'One Unicorn' = [Extent1].[Title]) AND ([Extent1].[Title] IS NOT NULL)'
Context 'BlogContext' is executing command 'SELECT [Extent1].[Id] AS [Id], [Extent1].[Title] AS [Title], [Extent1].[BlogId] AS [BlogId]FROM [dbo].[Posts] AS [Extent1]WHERE [Extent1].[BlogId] = @EntityKeyValue1'
Context 'BlogContext' is executing command 'update [dbo].[Posts]set [Title] = @0where ([Id] = @1)'
Context 'BlogContext' is executing command 'insert [dbo].[Posts]([Title], [BlogId])values (@0, @1)select [Id]from [dbo].[Posts]where @@rowcount > 0 and [Id] = scope_identity()'

Kesme yapı taşları

Şimdiye kadar EF tarafından oluşturulan SQL'i günlüğe kaydetmek için DbContext.Database.Log'un nasıl kullanılacağını inceledik. Ancak bu kod aslında daha genel kesme için bazı düşük düzeyli yapı taşlarının üzerinde nispeten ince bir cephedir.

Kesme arabirimleri

Kesme kodu, kesme arabirimleri kavramına göre oluşturulur. Bu arabirimler IDbInterceptor'dan devralır ve EF bazı eylemler gerçekleştirdiğinde çağrılan yöntemleri tanımlar. Amaç, kesişen nesne türü başına bir arabirime sahip olmaktır. Örneğin, IDbCommandInterceptor arabirimi EF'in ExecuteNonQuery, ExecuteScalar, ExecuteReader ve ilgili yöntemlere çağrı yapmasından önce çağrılan yöntemleri tanımlar. Benzer şekilde, arabirim bu işlemlerin her biri tamamlandığında çağrılan yöntemleri tanımlar. Yukarıda baktığımız DatabaseLogFormatter sınıfı, günlük komutlarına bu arabirimi uygular.

Kesme bağlamı

Kesme noktası arabirimlerinden herhangi birinde tanımlanan yöntemlere bakıldığında, her çağrıya DbInterceptionContext türünde bir nesne veya dbCommandInterceptionContext<> gibi bundan türetilmiş bir tür verildiği açıkça görülüyor. Bu nesne, EF'in gerçekleştirilen eylem hakkında bağlamsal bilgiler içerir. Örneğin, eylem bir DbContext adına gerçekleştiriliyorsa, DbContext DbInterceptionContext'e eklenir. Benzer şekilde, zaman uyumsuz olarak yürütülen komutlar için DbCommandInterceptionContext üzerinde IsAsync bayrağı ayarlanır.

Sonuç işleme

DbCommandInterceptionContext<> sınıfı Result, OriginalResult, Exception ve OriginalException adlı özellikler içerir. Bu özellikler, işlem yürütülmeden önce çağrılan kesme yöntemlerine yönelik çağrılar için null/sıfır olarak ayarlanır; diğer bir deyişle... Yöntemler yürütülüyor. İşlem yürütülür ve başarılı olursa Result ve OriginalResult işlemin sonucuna ayarlanır. Bu değerler daha sonra işlem yürütüldükten sonra çağrılan kesme yöntemlerinde gözlemlenebilir; diğer bir deyişle,... Yürütülen yöntemler. Benzer şekilde, işlem oluşturursa Exception ve OriginalException özellikleri ayarlanır.

Yürütmeyi gizleme

Bir kesme noktası, komut yürütülmeden önce Result özelliğini ayarlarsa (... Yöntemleri yürütme) ardından EF komutu gerçekten yürütmeyi denemez, bunun yerine yalnızca sonuç kümesini kullanır. Başka bir deyişle, kesme noktası komutun yürütülmesini gizler, ancak EF'nin komutu yürütülür gibi devam ettirebilir.

Bunun nasıl kullanılabileceğini gösteren bir örnek, geleneksel olarak bir sarmalama sağlayıcısıyla yapılan komut toplu işlemidir. Kesme noktası daha sonra yürütülecek komutu toplu iş olarak depolar ancak komutun normal şekilde yürütüldüğünü EF'e "taklit eder". Toplu işlem gerçekleştirmek için bundan daha fazlasının gerekli olduğunu unutmayın, ancak bu, kesme noktası sonucunu değiştirmenin nasıl kullanılabileceğini gösteren bir örnektir.

Yürütme, ... Yöntemler yürütülüyor. Bu, EF'nin belirtilen özel durum oluşturularak işlemin yürütülmesi başarısız olmuş gibi devam etmesine neden olur. Bu, elbette uygulamanın kilitlenmesine neden olabilir, ancak geçici bir özel durum veya EF tarafından işlenen başka bir özel durum da olabilir. Örneğin, bu komut yürütme başarısız olduğunda bir uygulamanın davranışını test etmek için test ortamlarında kullanılabilir.

Yürütmeden sonra sonucu değiştirme

Bir kesme noktası, komut yürütüldükten sonra Result özelliğini ayarlarsa (... Yürütülen yöntemler) ardından EF, işlemden gerçekten döndürülen sonuç yerine değiştirilen sonucu kullanır. Benzer şekilde, bir kesme noktası komut yürütüldükten sonra Exception özelliğini ayarlarsa EF, işlem özel durumu oluşturmuş gibi küme özel durumunu oluşturur.

Bir kesme noktası, özel durum oluşturmaması gerektiğini belirtmek için Exception özelliğini null olarak da ayarlayabilir. Bu, işlemin yürütülmesi başarısız olduysa ancak kesme noktası EF'nin işlem başarılı olmuş gibi devam etmesi için istekte bulunursa yararlı olabilir. Bu genellikle, EF'in devam ettikçe üzerinde çalışabilecek bazı sonuç değerlerine sahip olması için Sonucun ayarlanmasını da içerir.

OriginalResult ve OriginalException

EF bir işlemi yürüttkten sonra yürütme başarısız olmazsa Result ve OriginalResult özelliklerini ya da yürütme bir özel durumla başarısız olduysa Exception ve OriginalException özelliklerini ayarlar.

OriginalResult ve OriginalException özellikleri salt okunur olur ve yalnızca bir işlem yürütülürken EF tarafından ayarlanır. Bu özellikler kesişenler tarafından ayarlanamaz. Bu, herhangi bir kesme noktasının, işlem yürütülürken oluşan gerçek özel durum veya sonucun aksine, başka bir kesme noktası tarafından ayarlanan bir özel durumu veya sonucu ayırt edebildiği anlamına gelir.

Kesme noktası oluşturucuları kaydetme

Kesme arabirimlerinden birini veya daha fazlasını uygulayan bir sınıf oluşturulduktan sonra DbInterception sınıfı kullanılarak EF ile kaydedilebilir. Örnek:

DbInterception.Add(new NLogCommandInterceptor());

Kesme çizgileri, DbConfiguration kod tabanlı yapılandırma mekanizması kullanılarak uygulama etki alanı düzeyinde de kaydedilebilir.

Örnek: NLog'a günlüğe kaydetme

Şimdi tüm bunları IDbCommandInterceptor ve NLog kullanarak aşağıdakiler için bir örnek oluşturalım:

  • Zaman uyumsuz olarak yürütülen herhangi bir komut için uyarı günlüğe kaydetme
  • Yürütürken atılan herhangi bir komut için hata günlüğe kaydetme

Aşağıda, yukarıda gösterildiği gibi kaydedilmesi gereken günlüğe kaydetme işlemini yapan sınıf aşağıdadır:

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

Bu kodun, bir komutun zaman uyumsuz olarak ne zaman yürütülmekte olduğunu bulmak ve bir komutu yürütürken bir hatanın ne zaman olduğunu bulmak için kesme bağlamını nasıl kullandığına dikkat edin.