這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這里是第四篇:MVC程序中實體框架的連接恢復和命令攔截
譯文版權所有,謝絕全文轉載——但您可以在您的網站上添加到該教程的鏈接。
到目前為止,應用程序已經可以在您本地機器上正常地運行。但如果您想將它發布在互聯網上以便更多的人來使用,您需要將程序部署到WEB服務器並將數據庫部署到數據庫服務器。
在本教程中,您將學習在將實體框架部署到雲環境時非常有價值的兩個特點:連接回復(瞬時錯誤的自動重試)和命令攔截(捕捉所有發送到數據庫的SQL查詢語句,以便將它們記錄在日志中或更改它們)。
注意:本節教程是可選的。如果您跳過本節,我們會在后續的教程中做一些細微的調整。
啟用連接恢復
當您將應用程序部署到Windows Azure時,您會將數據庫部署到Windows Azure SQL數據庫中——一個雲數據庫服務。和您將Web服務器和數據庫直接連接在同一個數據中心相比,連接一個雲數據庫服務更容易遇到瞬時連接錯誤。即使雲Web服務器和雲數據庫服務器在同一數據中心機房中,它們之間在出現大量數據連接時也很容易出現各種問題,比如負載均衡。
另外,雲服務器通常是由其他用戶共享的,這意味着可能會受到其它用戶的影響。您對數據庫的存取權限可能受到限制,當您嘗試頻繁的訪問數據庫服務器時也可能遇到基於SLA的帶寬限制。大多數連接問題都是在您連接到雲服務器時瞬時發生的,它們會嘗試在短時間內自動解決問題。所以當您嘗試連接數據庫並遇到一個錯誤,該錯誤很可能是瞬時的,當您重復嘗試后可能該錯誤就不再存在。您可以使用自動瞬時錯誤重試來提高您的客戶體驗。實體框架6中的連接恢復能自動對錯誤的SQL查詢進行重試。
連接恢復功能只能針對特定的數據庫服務進行正確的配置后才可用:
- 必須知道那些異常有可能是暫時的,您想要重試由於網絡連接而造成的錯誤,而不是編程Bug帶來的。
- 在失敗操作的間隔中必須等待適當的時間。批量重試時在線用戶可能會需要等待較長時間才能夠獲得響應。
- 需要設置一個適當的重試次數。在一個在線的應用程序中,您可能會進行多次重試。
您可以為任何實體框架提供程序支持的數據庫環境來手動配置這些設定,但實體框架已經為使用Windows Azure SQL數據庫的在線應用程序做了缺省配置。接下來我們將在Contoso大學中實施這些配置。
如果要啟用連接恢復,您需要在您的程序集中創建一個從DbConfiguration派生的類,該類將用來配置SQL數據庫執行策略,其中包含連接恢復的重試策略。
- 在DAL文件夾中,添加一個名為SchoolConfiguration.cs的新類。
- 使用以下代碼替換類中的:
1 using System.Data.Entity; 2 using System.Data.Entity.SqlServer; 3 4 namespace ContosoUniversity.DAL 5 { 6 public class SchoolConfiguration : DbConfiguration 7 { 8 public SchoolConfiguration() 9 { 10 SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy()); 11 } 12 } 13 }
實體框架會自動運行從DbConfiguration類派生的類中找到的代碼,你同樣也可以使用Dbconfiguration類來在web.config中進行配置,詳細信息請參閱EntityFramework Code-Based Configuration。
- 在學生控制器中,添加引用:
using System.Data.Entity.Infrastructure;
- 更改所有捕獲DataException的異常代碼塊,使用RetryLimitExcededException:
catch (RetryLimitExceededException) { ModelState.AddModelError("", "保存數據時出現錯誤。請重試,如果問題依舊存在請聯系系統管理員。"); }
在之前,你使用了DataException。這樣會嘗試找出可能包含瞬時錯誤的異常,然后返回給用戶一個友好的重試提示消息,但現在你已經開啟自動重試策略,多次重試仍然失敗的錯誤將被包裝在RetryLimitExceededException異常中返回。
有關詳細信息,請參閱Entity Framework Connection Resiliency / Retry Logic。
啟用命令攔截
現在你已經打開了重試策略,但你如何進行測試已驗證它是否像預期的那樣正常工作?強迫發出一個瞬時錯誤並不容易,尤其是您正在本地運行的時候。而且瞬時錯誤也難以融入自動化的單元測試中。如果要測試連接恢復功能,您需要一種可以攔截實體框架發送到SQL數據庫查詢的方法並替代SQL數據庫返回響應。
你也可以在一個雲應用程序上按照最佳做法:log the latency and success or failure of all calls to external services來實現查詢攔截。實體框架6提供了一個dedicated logging API使它易於記錄。但在本教程中,您將學習如何直接使用實體框架的interception feature(攔截功能),包括日志記錄和模擬瞬時錯誤。
創建一個日志記錄接口和類
best practice for logging是通過接口而不是使用硬編碼調用System.Diagnostice.Trace或日志記錄類。這樣可以使得以后在需要時更容易地更改日志記錄機制。所以在本節中,我們將創建一個接口並實現它。
- 在項目中創建一個文件夾並命名為Logging。
- 在Logging文件夾中,創建一個名為ILogger.cs的接口類,使用下面的代碼替換自動生成的:
1 using System; 2 3 4 namespace ContosoUniversity.Logging 5 { 6 public interface ILogger 7 { 8 void Information(string message); 9 void Information(string fmt, params object[] vars); 10 void Information(Exception exception, string fmt, params object[] vars); 11 12 void Warning(string message); 13 void Warning(string fmt, params object[] vars); 14 void Warning(Exception exception, string fmt, params object[] vars); 15 16 void Error(string message); 17 void Error(string fmt, params object[] vars); 18 void Error(Exception exception, string fmt, params object[] vars); 19 20 void TraceApi(string componentName, string method, TimeSpan timespan); 21 void TraceApi(string componentName, string method, TimeSpan timespan, string properties); 22 void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars); 23 24 25 } 26 }
該接口提供了三個跟蹤級別用來指示日志的相對重要性,並且設計為可以提供外部服務調用(例如數據庫查詢)的延遲信息。日志方法提供了可以讓你傳遞異常的重載。這樣異常信息可以包含在棧中並且內部異常能夠可靠地被該接口實現的類記錄下來,而不是依靠從應用程序的每個日志方法來調用並記錄。
TraceAPI方法使您能夠跟蹤到外部服務(例如SQL Server)的每次調用的延遲時間。 - 在Logging文件夾中,創建一個名為Logger.cs的類,使用下面的代碼替換自動生成的:
1 using System; 2 using System.Diagnostics; 3 using System.Text; 4 5 namespace ContosoUniversity.Logging 6 { 7 public class Logger : ILogger 8 { 9 10 public void Information(string message) 11 { 12 Trace.TraceInformation(message); 13 } 14 15 public void Information(string fmt, params object[] vars) 16 { 17 Trace.TraceInformation(fmt, vars); 18 } 19 20 public void Information(Exception exception, string fmt, params object[] vars) 21 { 22 Trace.TraceInformation(FormatExceptionMessage(exception, fmt, vars)); 23 } 24 25 public void Warning(string message) 26 { 27 Trace.TraceWarning(message); 28 } 29 30 public void Warning(string fmt, params object[] vars) 31 { 32 Trace.TraceWarning(fmt, vars); 33 } 34 35 public void Warning(Exception exception, string fmt, params object[] vars) 36 { 37 throw new NotImplementedException(); 38 } 39 40 public void Error(string message) 41 { 42 Trace.TraceError(message); 43 } 44 45 public void Error(string fmt, params object[] vars) 46 { 47 Trace.TraceError(fmt, vars); 48 } 49 50 public void Error(Exception exception, string fmt, params object[] vars) 51 { 52 Trace.TraceError(FormatExceptionMessage(exception, fmt, vars)); 53 } 54 55 56 57 public void TraceApi(string componentName, string method, TimeSpan timespan) 58 { 59 TraceApi(componentName, method, timespan, ""); 60 } 61 62 public void TraceApi(string componentName, string method, TimeSpan timespan, string properties) 63 { 64 string message = String.Concat("Component:", componentName, ";Method:", method, ";Timespan:", timespan.ToString(), ";Properties:", properties); 65 Trace.TraceInformation(message); 66 } 67 68 public void TraceApi(string componentName, string method, TimeSpan timespan, string fmt, params object[] vars) 69 { 70 TraceApi(componentName, method, timespan, string.Format(fmt, vars)); 71 } 72 private string FormatExceptionMessage(Exception exception, string fmt, object[] vars) 73 { 74 var sb = new StringBuilder(); 75 sb.Append(string.Format(fmt, vars)); 76 sb.Append(" Exception: "); 77 sb.Append(exception.ToString()); 78 return sb.ToString(); 79 } 80 } 81 }
我們使用了System.Diagnostics來進行跟蹤。這是.Net的使它易於生成並使用跟蹤信息的一個內置功能。你可以使用System.Diagnostics的多種偵聽器來進行跟蹤並寫入日志文件。例如,將它們存入blob storage或存儲在Windows Azure。在 Troubleshooting Windows Azure Web Sites in Visual Studio中你可以找到更多選項及相關信息。在本教程中您將只在VS輸出窗口看到日志。
在生產環境中您可能想要使用跟蹤包而非System.Diagnostics,並且而當你需要時,ILogger接口能夠使它相對容易地切換到不同的跟蹤機制下。
創建攔截器類
接下來您將創建幾個類,這些類在實體框架在每次查詢數據庫時都會被調用。其中一個模擬瞬時錯誤而另一個進行日志記錄。這些攔截器類必須從DbCommandInterceptor類派生。你需要重寫方法使得查詢執行時會自動調用。在這些方法中您可以檢查或記錄被發往數據庫中的查詢,並且可以再查詢發送到數據庫之前對它們進行修改,甚至不將它們發送到數據庫進行查詢而直接返回結果給實體框架。
- 在DAL文件夾中創建一個名為SchoolInterceptorLogging.cs的類,並使用下面的代碼替換自動生成的:
1 using System; 2 using System.Data.Common; 3 using System.Data.Entity; 4 using System.Data.Entity.Infrastructure.Interception; 5 using System.Data.Entity.SqlServer; 6 using System.Data.SqlClient; 7 using System.Diagnostics; 8 using System.Reflection; 9 using System.Linq; 10 using ContosoUniversity.Logging; 11 12 namespace ContosoUniversity.DAL 13 { 14 public class SchoolInterceptorLogging : DbCommandInterceptor 15 { 16 private ILogger _logger = new Logger(); 17 private readonly Stopwatch _stopwatch = new Stopwatch(); 18 19 public override void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) 20 { 21 base.ScalarExecuting(command, interceptionContext); 22 _stopwatch.Restart(); 23 } 24 25 public override void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) 26 { 27 _stopwatch.Stop(); 28 if (interceptionContext.Exception != null) 29 { 30 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText); 31 } 32 else 33 { 34 _logger.TraceApi("SQL Database", "SchoolInterceptor.ScalarExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText); 35 } 36 base.ScalarExecuted(command, interceptionContext); 37 } 38 public override void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) 39 { 40 base.NonQueryExecuting(command, interceptionContext); 41 _stopwatch.Restart(); 42 } 43 44 public override void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) 45 { 46 _stopwatch.Stop(); 47 if (interceptionContext.Exception != null) 48 { 49 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText); 50 } 51 else 52 { 53 _logger.TraceApi("SQL Database", "SchoolInterceptor.NonQueryExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText); 54 } 55 base.NonQueryExecuted(command, interceptionContext); 56 } 57 58 public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) 59 { 60 base.ReaderExecuting(command, interceptionContext); 61 _stopwatch.Restart(); 62 } 63 public override void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) 64 { 65 _stopwatch.Stop(); 66 if (interceptionContext.Exception != null) 67 { 68 _logger.Error(interceptionContext.Exception, "Error executing command: {0}", command.CommandText); 69 } 70 else 71 { 72 _logger.TraceApi("SQL Database", "SchoolInterceptor.ReaderExecuted", _stopwatch.Elapsed, "Command: {0}: ", command.CommandText); 73 } 74 base.ReaderExecuted(command, interceptionContext); 75 } 76 } 77 }
對於成功查詢的命令,這段代碼將相關信息及延時信息寫入日志中,對於異常,它將創建錯誤日志。
-
在DAL文件夾中創建一個名為SchoolInterceptorTransientErrors.cs的類,該類在當您輸入"Throw"到搜索框並進行查詢時生成虛擬的瞬時錯誤。使用以下代碼替換自動生成的:
1 using System; 2 using System.Data.Common; 3 using System.Data.Entity; 4 using System.Data.Entity.Infrastructure.Interception; 5 using System.Data.Entity.SqlServer; 6 using System.Data.SqlClient; 7 using System.Diagnostics; 8 using System.Reflection; 9 using System.Linq; 10 using ContosoUniversity.Logging; 11 12 namespace ContosoUniversity.DAL 13 { 14 public class SchoolInterceptorTransientErrors : DbCommandInterceptor 15 { 16 private int _counter = 0; 17 private ILogger _logger = new Logger(); 18 19 public override void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) 20 { 21 bool throwTransientErrors = false; 22 if (command.Parameters.Count > 0 && command.Parameters[0].Value.ToString() == "Throw") 23 { 24 throwTransientErrors = true; 25 command.Parameters[0].Value = "an"; 26 command.Parameters[1].Value = "an"; 27 } 28 29 if (throwTransientErrors && _counter < 4) 30 { 31 _logger.Information("Returning transient error for command: {0}", command.CommandText); 32 _counter++; 33 interceptionContext.Exception = CreateDummySqlException(); 34 } 35 } 36 37 private SqlException CreateDummySqlException() 38 { 39 // The instance of SQL Server you attempted to connect to does not support encryption 40 var sqlErrorNumber = 20; 41 42 var sqlErrorCtor = typeof(SqlError).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 7).Single(); 43 var sqlError = sqlErrorCtor.Invoke(new object[] { sqlErrorNumber, (byte)0, (byte)0, "", "", "", 1 }); 44 45 var errorCollection = Activator.CreateInstance(typeof(SqlErrorCollection), true); 46 var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.Instance | BindingFlags.NonPublic); 47 addMethod.Invoke(errorCollection, new[] { sqlError }); 48 49 var sqlExceptionCtor = typeof(SqlException).GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic).Where(c => c.GetParameters().Count() == 4).Single(); 50 var sqlException = (SqlException)sqlExceptionCtor.Invoke(new object[] { "Dummy", errorCollection, null, Guid.NewGuid() }); 51 52 return sqlException; 53 } 54 } 55 }
這段代碼僅重寫了用來返回多行查詢結果數據的ReaderExcuting方法。如果你想要檢查其他類型的連接恢復,你可以重寫如NonQueryExecuting和ScalarExecuting方法就像在日志攔截器中所做的那樣。
當您運行學生頁面並輸入"Throw"作為搜索字符串時,代碼將創建一個虛擬的SQL數據庫錯誤數20,被當做瞬時錯誤類型。目前公認的瞬時錯誤號碼有64,233,10053,10060,10928,10929,40197,40501及40613等,你可以檢查新版的SQL 數據庫來確認這些信息。這段代碼返回異常給實體框架而不是運行查詢並返回查詢結果。瞬時異常將返回4次然后代碼將正常運行並將查詢結果返回。
由於我們有全部的日志記錄,你可以看到實體框架進行了4次查詢才執行成功,而在應用程序中,唯一的區別是呈現頁面所花費的事件變長了。
實體框架的重試次數是可以配置的,在本代碼中我們設定了4,因為這是SQL數據庫執行策略的缺省值。如果您更改執行策略,你同樣需要更改現有的代碼來指定生成瞬時錯誤的次數。您同樣可以更改代碼來生成更多的異常來引發實體框架的RetryLimitExceededException異常。
您在搜索框中輸入的值將保存在command.Parameters[0]和command.Parameters[1]中(一個用於姓而另一個用於名)。當發現輸入值為"Throw"時,參數被替換為"an"從而查詢到一些學生並返回。
這僅僅只是一種通過應用程序的UI來對連接恢復進行測試的方法。您也可以針對更新來編寫代碼生成瞬時錯誤。 - 在Global.asax,添加下面的using語句:
using ContosoUniversity.DAL; using System.Data.Entity.Infrastructure.Interception;
- 將高亮的行添加到Application_Start方法中:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); DbInterception.Add(new SchoolInterceptorTransientErrors()); DbInterception.Add(new SchoolInterceptorLogging()); }
這些代碼會在實體框架將查詢發送給數據庫時啟動攔截器。請注意,因為你分別單獨創建了 攔截器類的瞬時錯誤及日志記錄,您可以獨立的禁用和啟用它們。
你可以在應用程序的任何地方使用DbInterception.Add方法添加攔截器,並不一定要在Applicetion_Start中來做。另一個選擇是將這段代碼放進之前你創建執行策略的DbConfiguration類中。
public class SchoolConfiguration : DbConfiguration { public SchoolConfiguration() { SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy()); DbInterception.Add(new SchoolInterceptorTransientErrors()); DbInterception.Add(new SchoolInterceptorLogging()); } }
不管你在何處放置這些代碼,要小心不要超過一次。對於相同的攔截器執行DbInterception.Add可能會使你得到額外的攔截器實例。例如,如果添加兩次日志記錄攔截器,您將看到查詢被記錄在兩個日志中。
您已經編寫完成了模擬瞬時錯誤的代碼,現在可以在用戶界面通過輸入一個不同的值來進行測試了。作為替代方法,您可以在攔截器中編寫不檢查特定參數值而直接生成瞬時錯誤的代碼,記得僅僅當你想要測試瞬時錯誤時才添加攔截器。
攔截器是按照Add方法的注冊順序執行的。根據你所要進行的操作,順序可能很重要。例如,第一個攔截器可能會更改CommandText屬性,而下一個攔截器獲取到的會是更改過的該屬性。
測試日志記錄和連接恢復
- 按下F5在調試模式下運行該程序,然后點擊學生選項卡。
- 檢查VS輸出窗口,查看跟蹤輸出,您可能要向上滾動窗口內容。
您可以看到實際被發送到數據庫的SQL查詢。 - 在學生索引頁中,輸入"Throw"進行查詢。
你會注意到瀏覽器會掛起幾秒鍾,顯然實體框架正在進行重試查詢。第一次重試發生速度很快,然后每次重試查詢都會增加一點等待事件。
當頁面執行完成后,檢查輸出窗口,你會看到相同的查詢嘗試了5次,前4次都返回了一個瞬時錯誤異常。對於每個瞬時錯誤,你在日志中看到異常的信息。
返回學生數據的查詢是參數化的:
SELECT TOP (3) [Project1].[ID] AS [ID], [Project1].[LastName] AS [LastName], [Project1].[FirstMidName] AS [FirstMidName], [Project1].[EnrollmentDate] AS [EnrollmentDate] FROM ( SELECT [Project1].[ID] AS [ID], [Project1].[LastName] AS [LastName], [Project1].[FirstMidName] AS [FirstMidName], [Project1].[EnrollmentDate] AS [EnrollmentDate], row_number() OVER (ORDER BY [Project1].[LastName] ASC) AS [row_number] FROM ( SELECT [Extent1].[ID] AS [ID], [Extent1].[LastName] AS [LastName], [Extent1].[FirstMidName] AS [FirstMidName], [Extent1].[EnrollmentDate] AS [EnrollmentDate] FROM [dbo].[Student] AS [Extent1] WHERE (( CAST(CHARINDEX(UPPER(@p__linq__0), UPPER([Extent1].[LastName])) AS int)) > 0) OR (( CAST(CHARINDEX(UPPER(@p__linq__1), UPPER([Extent1].[FirstMidName])) AS int)) > 0) ) AS [Project1] ) AS [Project1] WHERE [Project1].[row_number] > 0 ORDER BY [Project1].[LastName] ASC
你沒有在日志中記錄值的參數,當然你也可以選擇記錄。你可以在攔截器的方法中通過從DbCommand對象的參數屬性中獲取到屬性值。
請注意您不能重復該測試,除非你停止整個應用程序並重新啟動它。如果你想要能夠在單個應用程序的運行中進行多次測試,您可以編寫代碼來重置SchoolInterceptorTransientErrors中的錯誤計數器。 - 要查看執行策略的區別,注釋掉SchoolConfiguration.cs,然后關閉應用程序並重新啟動調試,運行學生索引頁面並輸入"Throw"進行搜索。
public SchoolConfiguration() { //SetExecutionStrategy("System.Data.SqlClient", () => new SqlAzureExecutionStrategy()); }
- 取消注釋並再試一次,了解之間的不同。
總結
在本節中你看到了如何啟用實體框架的連接恢復,記錄發送到數據庫的SQL查詢命令,在下一節中你會使用Code First Migrations來將其部署該應用程序到互聯網中。
作者信息
Tom Dykstra - Tom Dykstra是微軟Web平台及工具團隊的高級程序員,作家。