Mencatat dan mencegat operasi database
Catatan
Hanya EF6 dan seterusnya - Fitur, API, dll. yang dibahas di halaman ini dimasukkan dalam Entity Framework 6. Jika Anda menggunakan versi yang lebih lama, beberapa atau semua informasi tidak berlaku.
Dimulai dengan Kerangka Kerja Entitas 6, kerangka kerja entitas kapan saja mengirim perintah ke database perintah ini dapat disadap oleh kode aplikasi. Ini paling umum digunakan untuk pengelogan SQL, tetapi juga dapat digunakan untuk memodifikasi atau membatalkan perintah.
Secara khusus, EF meliputi:
- Properti Log untuk konteks yang mirip dengan DataContext.Log in LINQ ke SQL
- Mekanisme untuk menyesuaikan konten dan pemformatan output yang dikirim ke log
- Blok penyusun tingkat rendah untuk intersepsi memberikan kontrol/fleksibilitas yang lebih besar
Properti Log Konteks
Properti DbContext.Database.Log dapat diatur ke delegasi untuk metode apa pun yang mengambil string. Paling umum digunakan dengan TextWriter apa pun dengan mengaturnya ke metode "Tulis" dari TextWriter tersebut. Semua SQL yang dihasilkan oleh konteks saat ini akan dicatat ke penulis tersebut. Misalnya, kode berikut akan mencatat SQL ke konsol:
using (var context = new BlogContext())
{
context.Database.Log = Console.Write;
// Your code here...
}
Perhatikan konteks tersebut. Database.Log diatur ke Console.Write. Ini semua yang diperlukan untuk mencatat SQL ke konsol.
Mari kita tambahkan beberapa kode kueri/sisipkan/perbarui sederhana sehingga kita dapat melihat beberapa output:
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();
}
Ini akan menghasilkan output berikut:
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
(Perhatikan bahwa ini adalah output dengan asumsi inisialisasi database apa pun telah terjadi. Jika inisialisasi database belum terjadi maka akan ada lebih banyak output yang menunjukkan semua Migrasi kerja yang dilakukan di bawah sampul untuk memeriksa atau membuat database baru.)
Apa yang dicatat?
Ketika properti Log diatur semua hal berikut ini akan dicatat:
- SQL untuk semua jenis perintah yang berbeda. Misalnya:
- Kueri, termasuk kueri LINQ normal, kueri eSQL, dan kueri mentah dari metode seperti SqlQuery
- Menyisipkan, memperbarui, dan menghapus yang dihasilkan sebagai bagian dari SaveChanges
- Kueri pemuatan hubungan seperti yang dihasilkan oleh pemuatan malas
- Parameter
- Apakah perintah sedang dijalankan secara asinkron atau tidak
- Tanda waktu yang menunjukkan kapan perintah mulai dijalankan
- Apakah perintah berhasil diselesaikan atau tidak, gagal dengan melempar pengecualian, atau, untuk asinkron, dibatalkan
- Beberapa indikasi nilai hasil
- Perkiraan jumlah waktu yang diperlukan untuk menjalankan perintah. Perhatikan bahwa ini adalah waktu dari mengirim perintah untuk mendapatkan objek hasil kembali. Ini tidak termasuk waktu untuk membaca hasilnya.
Melihat contoh output di atas, masing-masing dari empat perintah yang dicatat adalah:
- Kueri yang dihasilkan dari panggilan ke konteks. Blogs.First
- Perhatikan bahwa metode ToString untuk mendapatkan SQL tidak akan berfungsi untuk kueri ini karena "Pertama" tidak menyediakan IQueryable di mana ToString dapat dipanggil
- Kueri yang dihasilkan dari pemuatan blog yang malas. Posting
- Perhatikan detail parameter untuk nilai kunci tempat pemuatan malas terjadi
- Hanya properti parameter yang diatur ke nilai non-default yang dicatat. Misalnya, properti Ukuran hanya ditampilkan jika bukan nol.
- Dua perintah yang dihasilkan dari SaveChangesAsync; satu untuk pembaruan untuk mengubah judul postingan, yang lain untuk menyisipkan untuk menambahkan postingan baru
- Perhatikan detail parameter untuk properti FK dan Judul
- Perhatikan bahwa perintah ini sedang dijalankan secara asinkron
Pengelogan ke tempat yang berbeda
Seperti yang ditunjukkan di atas pengelogan ke konsol sangat mudah. Juga mudah untuk masuk ke memori, file, dll. dengan menggunakan berbagai jenis TextWriter.
Jika Anda terbiasa dengan LINQ ke SQL, Anda mungkin melihat bahwa di LINQ ke SQL, properti Log diatur ke objek TextWriter aktual (misalnya, Console.Out) saat berada di EF, properti Log diatur ke metode yang menerima string (misalnya, Console.Write atau Console.Out.Write). Alasannya adalah untuk memisahkan EF dari TextWriter dengan menerima delegasi apa pun yang dapat bertindak sebagai sink untuk string. Misalnya, bayangkan Anda sudah memiliki beberapa kerangka kerja pengelogan dan mendefinisikan metode pengelogan seperti itu:
public class MyLogger
{
public void Log(string component, string message)
{
Console.WriteLine("Component: {0} Message: {1} ", component, message);
}
}
Ini dapat dikaitkan ke properti Log EF seperti ini:
var logger = new MyLogger();
context.Database.Log = s => logger.Log("EFApp", s);
Pengelogan hasil
Logger default mencatat teks perintah (SQL), parameter, dan baris "Mengeksekusi" dengan tanda waktu sebelum perintah dikirim ke database. Baris "selesai" yang berisi waktu yang berlalu dicatat setelah eksekusi perintah.
Perhatikan bahwa untuk perintah asinkron baris "selesai" tidak dicatat sampai tugas asinkron benar-benar selesai, gagal, atau dibatalkan.
Baris "selesai" berisi informasi yang berbeda tergantung pada jenis perintah dan apakah eksekusi berhasil atau tidak.
Eksekusi berhasil
Untuk perintah yang berhasil menyelesaikan output adalah "Selesai dalam x ms dengan hasil: " diikuti oleh beberapa indikasi hasilnya. Untuk perintah yang mengembalikan pembaca data, indikasi hasilnya adalah jenis DbDataReader yang dikembalikan. Untuk perintah yang mengembalikan nilai bilangan bulat seperti perintah pembaruan yang ditunjukkan di atas hasil yang ditampilkan adalah bilangan bulat tersebut.
Eksekusi gagal
Untuk perintah yang gagal dengan melemparkan pengecualian, output berisi pesan dari pengecualian. Misalnya, menggunakan SqlQuery untuk mengkueri terhadap tabel yang memang ada akan menghasilkan output log seperti ini:
SELECT * from ThisTableIsMissing
-- Executing at 5/13/2013 10:19:05 AM
-- Failed in 1 ms with error: Invalid object name 'ThisTableIsMissing'.
Eksekusi dibatalkan
Untuk perintah asinkron di mana tugas dibatalkan, hasilnya bisa gagal dengan pengecualian, karena inilah yang sering dilakukan penyedia ADO.NET yang mendasar ketika upaya dilakukan untuk membatalkan. Jika ini tidak terjadi dan tugas dibatalkan dengan bersih maka output akan terlihat seperti ini:
update Blogs set Title = 'No' where Id = -1
-- Executing asynchronously at 5/13/2013 10:21:10 AM
-- Canceled in 1 ms
Mengubah konten log dan pemformatan
Di bawah sampul properti Database.Log gunakan objek DatabaseLogFormatter. Objek ini secara efektif mengikat implementasi IDbCommandInterceptor (lihat di bawah) ke delegasi yang menerima string dan DbContext. Ini berarti bahwa metode pada DatabaseLogFormatter dipanggil sebelum dan sesudah eksekusi perintah oleh EF. Metode DatabaseLogFormatter ini mengumpulkan dan memformat output log dan mengirimkannya ke delegasi.
Menyesuaikan DatabaseLogFormatter
Mengubah apa yang dicatat dan bagaimana diformat dapat dicapai dengan membuat kelas baru yang berasal dari DatabaseLogFormatter dan mengambil alih metode yang sesuai. Metode yang paling umum untuk diambil alih adalah:
- LogCommand – Ambil alih ini untuk mengubah cara perintah dicatat sebelum dijalankan. Secara default LogCommand memanggil LogParameter untuk setiap parameter; Anda dapat memilih untuk melakukan hal yang sama dalam mengambil alih atau menangani parameter secara berbeda sebagai gantinya.
- LogResult – Ambil alih ini untuk mengubah bagaimana hasil dari menjalankan perintah dicatat.
- LogParameter – Ambil alih ini untuk mengubah pemformatan dan konten pengelogan parameter.
Misalnya, kita ingin mencatat hanya satu baris sebelum setiap perintah dikirim ke database. Ini dapat dilakukan dengan dua penimpaan:
- Ganti LogCommand untuk memformat dan menulis baris tunggal SQL
- Ambil alih LogResult untuk tidak melakukan apa-apa.
Kode akan terlihat seperti ini:
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)
{
}
}
Untuk mencatat output cukup panggil metode Tulis yang akan mengirim output ke delegasi tulis yang dikonfigurasi.
(Perhatikan bahwa kode ini melakukan penghapusan pemutusan baris yang sederhana hanya sebagai contoh. Kemungkinan tidak akan berfungsi dengan baik untuk melihat SQL yang kompleks.)
Mengatur DatabaseLogFormatter
Setelah kelas DatabaseLogFormatter baru dibuat, kelas tersebut perlu didaftarkan ke EF. Ini dilakukan menggunakan konfigurasi berbasis kode. Singkatnya, ini berarti membuat kelas baru yang berasal dari DbConfiguration dalam rakitan yang sama dengan kelas DbContext Anda dan kemudian memanggil SetDatabaseLogFormatter di konstruktor kelas baru ini. Contohnya:
public class MyDbConfiguration : DbConfiguration
{
public MyDbConfiguration()
{
SetDatabaseLogFormatter(
(context, writeAction) => new OneLineFormatter(context, writeAction));
}
}
Menggunakan DatabaseLogFormatter baru
DatabaseLogFormatter baru ini sekarang akan digunakan kapan saja Database.Log diatur. Jadi, menjalankan kode dari bagian 1 sekarang akan menghasilkan output berikut:
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()'
Blok penyusun intersepsi
Sejauh ini kita telah melihat cara menggunakan DbContext.Database.Log untuk mencatat SQL yang dihasilkan oleh EF. Tetapi kode ini sebenarnya adalah fasad yang relatif tipis atas beberapa blok penyusun tingkat rendah untuk intersepsi yang lebih umum.
Antarmuka intersepsi
Kode intersepsi dibangun di sekitar konsep antarmuka intersepsi. Antarmuka ini mewarisi dari IDbInterceptor dan menentukan metode yang dipanggil ketika EF melakukan beberapa tindakan. Niatnya adalah untuk memiliki satu antarmuka per jenis objek yang dicegat. Misalnya, antarmuka IDbCommandInterceptor menentukan metode yang dipanggil sebelum EF melakukan panggilan ke ExecuteNonQuery, ExecuteScalar, ExecuteReader, dan metode terkait. Demikian juga, antarmuka mendefinisikan metode yang dipanggil ketika masing-masing operasi ini selesai. Kelas DatabaseLogFormatter yang kita lihat di atas mengimplementasikan antarmuka ini ke perintah log.
Konteks penyadapan
Melihat metode yang ditentukan pada salah satu antarmuka pencegat, jelas bahwa setiap panggilan diberikan objek jenis DbInterceptionContext atau beberapa jenis yang berasal dari ini seperti DbCommandInterceptionContext<>. Objek ini berisi informasi kontekstual tentang tindakan yang diambil EF. Misalnya, jika tindakan diambil atas nama DbContext, maka DbContext disertakan dalam DbInterceptionContext. Demikian pula, untuk perintah yang sedang dijalankan secara asinkron, bendera IsAsync diatur pada DbCommandInterceptionContext.
Penanganan hasil
Kelas DbCommandInterceptionContext<> berisi properti yang disebut Result, OriginalResult, Exception, dan OriginalException. Properti ini diatur ke null/nol untuk panggilan ke metode intersepsi yang dipanggil sebelum operasi dijalankan — yaitu, untuk ... Menjalankan metode. Jika operasi dijalankan dan berhasil, maka Result dan OriginalResult diatur ke hasil operasi. Nilai-nilai ini kemudian dapat diamati dalam metode intersepsi yang dipanggil setelah operasi dijalankan — yaitu, pada ... Metode yang dijalankan. Demikian juga, jika operasi dilemparkan, maka properti Exception dan OriginalException akan diatur.
Menekan eksekusi
Jika pencegat mengatur properti Hasil sebelum perintah dijalankan (di salah satu ... Menjalankan metode) maka EF tidak akan mencoba untuk benar-benar menjalankan perintah, tetapi hanya akan menggunakan tataan hasil. Dengan kata lain, pencegat dapat menekan eksekusi perintah tetapi meminta EF berlanjut seolah-olah perintah telah dijalankan.
Contoh bagaimana ini dapat digunakan adalah batching perintah yang secara tradisional telah dilakukan dengan penyedia pembungkus. Pencegat akan menyimpan perintah untuk eksekusi nanti sebagai batch tetapi akan "berpura-pura" kepada EF bahwa perintah telah dijalankan seperti biasa. Perhatikan bahwa diperlukan lebih dari ini untuk menerapkan batching, tetapi ini adalah contoh bagaimana mengubah hasil intersepsi mungkin digunakan.
Eksekusi juga dapat ditekan dengan mengatur properti Pengecualian di salah satu ... Menjalankan metode. Hal ini menyebabkan EF melanjutkan seolah-olah eksekusi operasi telah gagal dengan melemparkan pengecualian yang diberikan. Ini mungkin, tentu saja, menyebabkan aplikasi mengalami crash, tetapi mungkin juga merupakan pengecualian sementara atau beberapa pengecualian lain yang ditangani oleh EF. Misalnya, ini dapat digunakan di lingkungan pengujian untuk menguji perilaku aplikasi saat eksekusi perintah gagal.
Mengubah hasil setelah eksekusi
Jika pencegat mengatur properti Hasil setelah perintah dijalankan (di salah satu ... Metode yang dijalankan) maka EF akan menggunakan hasil yang diubah alih-alih hasil yang benar-benar dikembalikan dari operasi. Demikian pula, jika pencegat menetapkan properti Pengecualian setelah perintah dijalankan, maka EF akan melemparkan pengecualian yang ditetapkan seolah-olah operasi telah melemparkan pengecualian.
Pencegat juga dapat mengatur properti Pengecualian ke null untuk menunjukkan bahwa tidak ada pengecualian yang harus dilemparkan. Ini dapat berguna jika eksekusi operasi gagal tetapi pencegat ingin EF melanjutkan seolah-olah operasi telah berhasil. Ini biasanya juga melibatkan pengaturan Hasil sehingga EF memiliki beberapa nilai hasil untuk dikerjakan saat berlanjut.
OriginalResult dan OriginalException
Setelah EF menjalankan operasi, EF akan mengatur properti Result dan OriginalResult jika eksekusi tidak gagal atau properti Exception dan OriginalException jika eksekusi gagal dengan pengecualian.
Properti OriginalResult dan OriginalException bersifat baca-saja dan hanya ditetapkan oleh EF setelah benar-benar menjalankan operasi. Properti ini tidak dapat diatur oleh pencegat. Ini berarti bahwa setiap pencegat dapat membedakan antara pengecualian atau hasil yang telah ditetapkan oleh beberapa pencegat lain dibandingkan dengan pengecualian atau hasil nyata yang terjadi ketika operasi dijalankan.
Mendaftarkan pencegat
Setelah kelas yang mengimplementasikan satu atau beberapa antarmuka intersepsi telah dibuat, itu dapat didaftarkan ke EF menggunakan kelas DbInterception. Contohnya:
DbInterception.Add(new NLogCommandInterceptor());
Pencegat juga dapat didaftarkan di tingkat domain aplikasi menggunakan mekanisme konfigurasi berbasis kode DbConfiguration.
Contoh: Pengelogan ke NLog
Mari kita satukan semua ini ke dalam contoh yang menggunakan IDbCommandInterceptor dan NLog untuk:
- Catat peringatan untuk perintah apa pun yang dijalankan secara non-asinkron
- Mencatat kesalahan untuk perintah apa pun yang dilemparkan saat dijalankan
Berikut adalah kelas yang melakukan pengelogan, yang harus didaftarkan seperti yang ditunjukkan di atas:
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);
}
}
}
Perhatikan bagaimana kode ini menggunakan konteks intersepsi untuk menemukan ketika perintah dijalankan secara non-asinkron dan untuk menemukan kapan ada kesalahan saat menjalankan perintah.