記錄和攔截資料庫作業
注意
僅限 EF6 及更新版本 - Entity Framework 6 已引進此頁面中所討論的功能及 API 等等。 如果您使用的是較早版本,則不適用部分或全部的資訊。
從 Entity Framework 6 開始,每當 Entity Framework 傳送命令至資料庫時,應用程式程式碼就可以攔截此命令。 這最常用於記錄 SQL,但也可以用來修改或中止命令。
具體來說,EF 包含:
- 與 LINQ to SQL 中 DataCoNtext.Log 類似的內容的 Log 屬性
- 自訂傳送至記錄檔之輸出內容和格式設定的機制
- 用於攔截的低階建置組塊,提供更大的控制/彈性
CoNtext Log 屬性
DbCoNtext.Database.Log 屬性可以設定為接受字串之任何方法的委派。 最常見的方式是將其設定為該 TextWriter 的 「Write」 方法,以搭配任何 TextWriter 使用。 目前內容所產生的所有 SQL 都會記錄到該寫入器。 例如,下列程式碼會將 SQL 記錄到主控台:
using (var context = new BlogContext())
{
context.Database.Log = Console.Write;
// Your code here...
}
請注意該內容。Database.Log 設定為 Console.Write。 這是將 SQL 記錄至主控台所需的所有專案。
讓我們新增一些簡單的查詢/插入/更新程式碼,讓我們可以看到一些輸出:
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();
}
這會產生下列輸出:
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
(請注意,這是假設任何資料庫初始化已經發生的輸出。如果尚未進行資料庫初始化,則會有更多輸出顯示移轉在涵蓋範圍下的所有工作,以檢查或建立新的資料庫。
會記錄哪些內容?
設定 Log 屬性時,將會記錄下列所有專案:
- 適用于各種不同命令的 SQL。 例如:
- 查詢,包括一般 LINQ 查詢、eSQL 查詢,以及 SqlQuery 等方法的原始查詢
- 在 SaveChanges 中產生的插入、更新和刪除
- 關聯性載入查詢,例如延遲載入所產生的查詢
- 參數
- 命令是否以非同步方式執行
- 時間戳記,指出命令何時開始執行
- 命令是否已順利完成、擲回例外狀況或非同步失敗
- 結果值的一些指示
- 執行命令所花費的大約時間。 請注意,這是將命令傳送回結果物件的時間。 不包含讀取結果的時間。
查看上述範例輸出,記錄的四個命令各有:
- 呼叫內容所產生的查詢。Blogs.First
- 請注意,取得 SQL 的 ToString 方法無法用於此查詢,因為 「First」 不提供可呼叫 ToString 的 IQueryable
- 由部落格延遲載入所產生的查詢。職位
- 請注意延遲載入發生之索引鍵值的參數詳細資料
- 只會記錄設定為非預設值的參數屬性。 例如,只有在 Size 屬性為非零時才會顯示。
- SaveChangesAsync 所產生的兩個命令;其中一個用於變更文章標題的更新,另一個用於插入以新增文章
- 請注意 FK 和 Title 屬性的參數詳細資料
- 請注意,這些命令正以非同步方式執行
記錄到不同位置
如上所示,登入主控台是超級容易的。 使用不同類型的 TextWriter 也很容易記錄到記憶體、檔案等。
如果您熟悉 LINQ to SQL,您可能會注意到在 LINQ to SQL 中,Log 屬性會設定為實際的 TextWriter 物件(例如 Console.Out),而 EF 中的 Log 屬性會設定為接受字串的方法(例如 Console.Write 或 Console.Out.Write)。 原因是接受任何可做為字串接收的委派,讓 EF 與 TextWriter 分離。 例如,假設您已經有一些記錄架構,它會定義記錄方法,如下所示:
public class MyLogger
{
public void Log(string component, string message)
{
Console.WriteLine("Component: {0} Message: {1} ", component, message);
}
}
這可能會連結至 EF Log 屬性,如下所示:
var logger = new MyLogger();
context.Database.Log = s => logger.Log("EFApp", s);
結果記錄
預設記錄器會記錄命令文字 (SQL)、參數,以及具有時間戳記的「執行」行,再將命令傳送至資料庫。 在命令執行之後,會記錄包含經過時間的「已完成」行。
請注意,在非同步工作實際完成、失敗或取消之前,不會記錄非同步命令的「已完成」行。
「已完成」行包含不同的資訊,視命令類型以及執行是否成功而定。
成功執行
對於成功完成的命令,輸出為「在 x ms 中完成,結果為:」,後面接著一些結果的指示。 對於傳回資料讀取器的命令,結果指示是傳回的 DbDataReader 類型。 對於傳回整數值的命令,例如高於所顯示結果的 update 命令是該整數。
執行失敗
對於擲回例外狀況而失敗的命令,輸出會包含來自例外狀況的訊息。 例如,使用 SqlQuery 針對存在的資料表進行查詢,將會導致記錄輸出如下:
SELECT * from ThisTableIsMissing
-- Executing at 5/13/2013 10:19:05 AM
-- Failed in 1 ms with error: Invalid object name 'ThisTableIsMissing'.
已取消執行
針對取消工作的非同步命令,結果可能會因為例外狀況而失敗,因為這是基礎 ADO.NET 提供者在嘗試取消時經常執行的動作。 如果這種情況未發生,且工作會完全取消,則輸出看起來會像這樣:
update Blogs set Title = 'No' where Id = -1
-- Executing asynchronously at 5/13/2013 10:21:10 AM
-- Canceled in 1 ms
變更記錄內容和格式
在 底下,Database.Log 屬性會使用 DatabaseLogFormatter 物件。 此物件會將 IDbCommandInterceptor 實作有效地系結至接受字串和 DbCoNtext 的委派。 這表示在 EF 執行命令之前和之後,會呼叫 DatabaseLogFormatter 上的方法。 這些 DatabaseLogFormatter 方法會收集並格式化記錄輸出,並將其傳送至委派。
自訂 DatabaseLogFormatter
藉由建立衍生自 DatabaseLogFormatter 的新類別,並適當地覆寫方法,來變更所記錄的內容及其格式化方式。 要覆寫的最常見方法是:
- LogCommand – 覆寫此選項,以變更命令在執行之前記錄的方式。 根據預設,LogCommand 會針對每個參數呼叫 LogParameter;您可以選擇在覆寫中執行相同的動作,或改為以不同的方式處理參數。
- LogResult – 覆寫此選項,以變更執行命令的結果記錄方式。
- LogParameter – 覆寫此選項以變更參數記錄的格式和內容。
例如,假設我們想要在將每個命令傳送至資料庫之前只記錄一行。 這可以使用兩個覆寫來完成:
- 覆寫 LogCommand 以格式化和寫入 SQL 的單行
- 覆寫 LogResult 以不執行任何動作。
程式碼應該類似如下所示:
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)
{
}
}
若要記錄輸出,只需呼叫 Write 方法,將輸出傳送至設定的寫入委派。
(請注意,此程式碼會簡化分行符號的移除,就像範例一樣。檢視複雜 SQL 可能無法正常運作。
設定 DatabaseLogFormatter
建立新的 DatabaseLogFormatter 類別之後,就必須向 EF 註冊。 這是使用程式碼型組態完成的。 簡言之,這表示在與 DbCoNtext 類別相同的元件中建立衍生自 DbConfiguration 的新類別,然後在這個新類別的建構函式中呼叫 SetDatabaseLogFormatter。 例如:
public class MyDbConfiguration : DbConfiguration
{
public MyDbConfiguration()
{
SetDatabaseLogFormatter(
(context, writeAction) => new OneLineFormatter(context, writeAction));
}
}
使用新的 DatabaseLogFormatter
每當設定 Database.Log 時,就會使用這個新的 DatabaseLogFormatter。 因此,從第 1 部分執行程式碼現在會產生下列輸出:
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()'
攔截建置組塊
到目前為止,我們已探討如何使用 DbCoNtext.Database.Log 來記錄 EF 所產生的 SQL。 但是,此程式碼實際上是在一些低階建置組塊上相對稀薄的外牆,用於更一般的攔截。
攔截介面
攔截程式碼是以攔截介面的概念為基礎所建置。 這些介面繼承自 IDbInterceptor,並定義 EF 執行某些動作時呼叫的方法。 意圖是讓每個類型的物件都有一個介面被攔截。 例如,IDbCommandInterceptor 介面會定義在 EF 呼叫 ExecuteNonQuery、ExecuteScalar、ExecuteReader 和相關方法之前呼叫的方法。 同樣地,介面會定義每個作業完成時所呼叫的方法。 我們查看上述的 DatabaseLogFormatter 類別會實作此介面來記錄命令。
攔截內容
查看在任何攔截器介面上定義的方法,很明顯,每個呼叫都會獲得 DbInterceptionCoNtext 類型的物件,或衍生自這類 DbCommandInterceptionCoNtext <> 的某些類型。 此物件包含 EF 所採取之動作的相關內容資訊。 例如,如果要代表 DbCoNtext 採取動作,則 DbCoNtext 會包含在 DbInterceptionCoNtext 中。 同樣地,對於以非同步方式執行的命令,IsAsync 旗標是在 DbCommandInterceptionCoNtext 上設定。
結果處理
DbCommandInterceptionCoNtext <> 類別包含名為 Result、OriginalResult、Exception 和 OriginalException 的屬性。 這些屬性會針對在執行作業之前呼叫的攔截方法呼叫設定為 null/零, 也就是 ...執行方法。 如果作業執行且成功,則 Result 和 OriginalResult 會設定為作業的結果。 然後,您可以在執行作業之後呼叫的攔截方法中觀察到這些值,也就是 ...已執行的方法。 同樣地,如果作業擲回,則會設定 Exception 和 OriginalException 屬性。
隱藏執行
如果攔截器在命令執行之前設定 Result 屬性(在其中一個 ...執行方法)然後 EF 不會嘗試實際執行命令,而是只會使用結果集。 換句話說,攔截器可以隱藏命令的執行,但讓 EF 繼續,就像已執行命令一樣。
使用這個方法的範例是傳統上透過包裝提供者完成的命令批次處理。 攔截器會將命令儲存為後續執行的批次,但會「假裝」到 EF,命令已正常執行。 請注意,它需要更多才能實作批次處理,但這是如何使用變更攔截結果的範例。
您也可以在其中一個 ... 中設定 Exception 屬性來隱藏執行。執行方法。 這會導致 EF 繼續執行作業,就像擲回指定的例外狀況而失敗一樣。 當然,這可能會造成應用程式當機,但也可能是暫時性例外狀況或 EF 所處理的一些其他例外狀況。 例如,這可用於測試環境中,以在命令執行失敗時測試應用程式的行為。
在執行後變更結果
如果攔截器在執行命令之後設定 Result 屬性(在其中一個 ...執行的方法)然後 EF 會使用已變更的結果,而不是實際從作業傳回的結果。 同樣地,如果攔截器在執行命令之後設定 Exception 屬性,則 EF 會擲回設定例外狀況,就像作業擲回例外狀況一樣。
攔截器也可以將 Exception 屬性設定為 null,表示不應擲回例外狀況。 如果作業執行失敗,但攔截器希望 EF 繼續,就如同作業成功一樣,這非常有用。 這通常也牽涉到設定 Result,讓 EF 有一些結果值可隨著其繼續運作。
OriginalResult 和 OriginalException
在 EF 執行作業之後,如果執行未失敗,則會設定 Result 和 OriginalResult 屬性,如果執行失敗,則會設定 Exception 和 OriginalException 屬性,但發生例外狀況。
OriginalResult 和 OriginalException 屬性是唯讀的,只有在實際執行作業之後,才會由 EF 設定。 攔截器無法設定這些屬性。 這表示任何攔截器都可以區分其他攔截器所設定的例外狀況或結果,而不是執行作業時所發生的實際例外狀況或結果。
註冊攔截器
一旦實作一或多個攔截介面的類別已建立,就可以使用 DbInterception 類別向 EF 註冊。 例如:
DbInterception.Add(new NLogCommandInterceptor());
攔截器也可以使用 DbConfiguration 程式碼型組態機制,在應用程式網域層級註冊。
範例:記錄至 NLog
讓我們將這一切放在使用 IDbCommandInterceptor 和 NLog 的範例中:
- 記錄非非同步執行之任何命令的警告
- 針對執行時擲回的任何命令記錄錯誤
以下是執行記錄的類別,應該註冊如上所示:
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);
}
}
}
請注意,此程式碼如何使用攔截內容來探索何時以非非同步方式執行命令,以及探索執行命令時發生錯誤。