NanoProfiler 對於資料庫存取的監控 - ADO.NET

這一篇來說明對於使用 ADO.NET 進行資料庫存取的 Repository 要如何使用 NanoProfiler.Data 去做到監控與執行 T-SQL 的記錄呈現。

前一篇「使用 NanoProfiler 對 ASP.NET Web API 進行性能監控」介紹了 NanoPrifiler 這個套件,讓我們可以在 ASP.NET MVC / WebAPI 或 WCF 等專案裡去監控程式執行的效能,因為是第一篇,所以只有簡單的說明怎麼在 Controller 類別的 Action 方法裡的使用方式,所以在 NanoProfiler 的 Profilering Result 頁面裡只有呈現 Controller 的 Action 裡被 ProfilingSession 給包起來的程式執行數據。

很多人誤會說只要在某個地方用程式給包起來之後就會將有用到的所有程式效能數據都能夠抓出來,這…… 真的有點誤會了,其實 NanoProfiler 並沒有如此厲害,要是真的那麼厲害的話,這樣的程式也太恐怖了些,其實效能數據的監控並不是全部程式的執行效能數據都抓出來就可以讓我們了解程式的狀況,因為太多的數據呈現反而會失去焦點與意義,而是應該對於執行效能有疑慮或是要詳加注意的部分再去做監控就可以。所以 NanoProfiler 的效能監控是由我們來決定那些程式需要被監控,有需要監控的地方再去把程式給包起來就好。

 

下圖是上次我們完成的 Profiling Result 內容

[image63.png]

我在專案裡有三種 Repository 的實作方式,分別是使用 ADO.NET, Enterprise Library Data Access Application Block, Entity Framework,而這一篇會針對使用 ADO.NET 實作的 Repository 來做說明。

image

在 Sample.Repository.ADONET 這個專案裡要使用 NuGet 安裝 NanoProfiler.Data 套件,如果你的專案並沒有另外切分出 Repository 專案的話,就看你的資料存取的程式是在哪一個專案裡,就那個專案也安裝 NanoProfiler.Data

image

 

裝好之後要幹嘛咧,首先來看看 NanoProfiler 的 Wiki 裡是怎麼寫的,

NanoProfiler Wiki - 4. How to enable DB profiling?

Wiki 裡面寫的範例是在 DemoDBDataService 類別裡有對資料庫作存取的操作,如下:

public class DemoDBDataService : IDemoDBDataService
{
    public List<DemoData> LoadActiveDemoData()
    {
        using (ProfilingSession.Current.Step("Data.LoadActiveDemoData"))
        {
            using (var conn = GetConnection())
            {
                conn.Open();
 
                using (var cmd = conn.CreateCommand())
                {
                    cmd.CommandType = CommandType.Text;
                    cmd.CommandText = "select Id, Name from [Table] where IsActive = @IsActive";
                    cmd.Parameters.Add(new SqlParameter("@IsActive", 1));
 
                    using (var reader = cmd.ExecuteReader())
                    {
                        var results = new List<DemoData>();
                        while (reader.Read())
                        {
                            results.Add(new DemoData { Id = reader.GetInt32(0), Name = reader.GetString(1) });
                        }
                        return results;
                    }
                }
            }
        }
    }
 
    private IDbConnection GetConnection()
    {
        var conn = new SqlConnection(@"Server=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\DemoDB.mdf;Database=DemoDB;Trusted_Connection=Yes;");
        return conn;
    }
}

然後在取得 SqlConnection 的部分是透過 GetConnection 方法,如下:

public class DemoDBDataService : IDemoDBDataService
{
    //...
 
    private IDbConnection GetConnection()
    {
        var conn = new SqlConnection(@"Server=(LocalDb)\v11.0;AttachDbFilename=|DataDirectory|\DemoDB.mdf;Database=DemoDB;Trusted_Connection=Yes;");
 
        if (ProfilingSession.Current == null)
        {
            return conn;
        }
 
        var dbProfiler = new DbProfiler(ProfilingSession.Current.Profiler);
        return new ProfiledDbConnection(conn, dbProfiler);
    }
}

程式裡需要建立 DbProfiler 然後再另外去使用原本的 SqlConnection 與 DbProfiler 建立 ProfiledDbConnection,如此一來才能讓 NanoProfiler 去監控操作資料存取的時間以及所執行的 T-SQL內容。

 

以下是我的範例程式的資料操作程式,這是取得所有的 Employee 資料,

/// <summary>
/// Gets the employees.
/// </summary>
/// <returns></returns>
/// <exception cref="System.NotImplementedException"></exception>
public IEnumerable<Employee> GetEmployees()
{
    List<Employee> employees = new List<Employee>();
 
    string sqlStatement = "select * from Employees order by EmployeeID";
 
    using (SqlConnection conn = new SqlConnection(this.ConnectionString))
    using (SqlCommand command = new SqlCommand(sqlStatement, conn))
    {
        command.CommandType = CommandType.Text;
        command.CommandTimeout = 180;
 
        if (conn.State != ConnectionState.Open) conn.Open();
 
        using (SqlDataReader reader = command.ExecuteReader())
        {
            while (reader.Read())
            {
                Employee item = new Employee();
 
                for (int i = 0; i < reader.FieldCount; i++)
                {
                    PropertyInfo property = item.GetType().GetProperty(reader.GetName(i));
 
                    if (property != null && !reader.GetValue(i).Equals(DBNull.Value))
                    {
                        ReflectionHelper.SetValue(property.Name, reader.GetValue(i), item);
                    }
                }
                employees.Add(item);
            }
        }
    }
 
    return employees;
}

這邊我會另去建立一個 DatabaseHelper,透過這個 Helper 類別取得 ProfiledDbConnection,

IDatabaseHelper.cs

using System.Data;
using EF.Diagnostics.Profiling;
 
namespace Sample.Repository.ADONET
{
    public interface IDatabaseHelper
    {
        /// <summary>
        /// Gets the profiled connection (with NanoProfiler).
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <param name="currentSession">The current session.</param>
        /// <returns>IDbConnection.</returns>
        IDbConnection GetProfiledConnection(
            string connectionString, ProfilingSession currentSession);
    }
}

DatabaseHelper.cs

using System.Data;
using System.Data.SqlClient;
using EF.Diagnostics.Profiling;
using EF.Diagnostics.Profiling.Data;
 
namespace Sample.Repository.ADONET
{
    public class DatabaseHelper : IDatabaseHelper
    {
        /// <summary>
        /// Gets the profiled connection (with NanoProfiler).
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <param name="currentSession">The current session.</param>
        /// <returns>IDbConnection.</returns>
        /// <exception cref="System.NotImplementedException"></exception>
        public IDbConnection GetProfiledConnection(
            string connectionString, ProfilingSession currentSession)
        {
            var profiler = GetProfiler(ProfilingSession.Current);
            var conn = this.GetConnection(connectionString, profiler);
            return conn;
        }
 
        /// <summary>
        /// Gets the connection.
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <param name="profiler">The profiler.</param>
        /// <returns>IDbConnection.</returns>
        private IDbConnection GetConnection(string connectionString, IProfiler profiler = null)
        {
            var conn = new SqlConnection(connectionString);
            if (profiler == null)
            {
                return conn;
            }
            var dbProfiler = new DbProfiler(profiler);
            return new ProfiledDbConnection(conn, dbProfiler);
        }
 
        /// <summary>
        /// Gets the profiler.
        /// </summary>
        /// <param name="currentSession">The current session.</param>
        /// <returns>IProfiler.</returns>
        private static IProfiler GetProfiler(ProfilingSession currentSession)
        {
            IProfiler profiler = currentSession == null
                                 ? null
                                 : currentSession.Profiler;
 
            return profiler;
        }
    }
}

 

在 EmployeeRepository.cs 類別裡的修改,如下:

GetOne()

image

/// <summary>
/// Gets the one.
/// </summary>
/// <param name="id">The id.</param>
/// <returns></returns>
/// <exception cref="System.NotImplementedException"></exception>
public Employee GetOne(int id)
{
    using (ProfilingSession.Current.Step("EmployeeRepository - GetOne"))
    {
        string sqlStatement = "select * from Employees where EmployeeID = @EmployeeID";
 
        Employee item = new Employee();
 
        var profiledDbConnection =
            this.DatabaseHelper.GetProfiledConnection(this.ConnectionString, ProfilingSession.Current);
 
        using (var conn = profiledDbConnection)
        {
            conn.Open();
 
            using (var command = conn.CreateCommand())
            {
                command.CommandType = CommandType.Text;
                command.CommandText = sqlStatement;
                command.Parameters.Add(new SqlParameter("EmployeeID", id));
 
                if (conn.State != ConnectionState.Open)
                {
                    conn.Open();
                }
 
                using (IDataReader reader = command.ExecuteReader())
                {
                    if (reader.Read())
                    {
                        for (int i = 0; i < reader.FieldCount; i++)
                        {
                            PropertyInfo property = item.GetType().GetProperty(reader.GetName(i));
 
                            if (property != null &&
                                !reader.GetValue(i).Equals(DBNull.Value))
                            {
                                ReflectionHelper.SetValue(property.Name, reader.GetValue(i), item);
                            }
                        }
                    }
                }
            }
        }
        return item;
    }
}

 

GetEmployees()

image

/// <summary>
/// Gets the employees.
/// </summary>
/// <returns></returns>
/// <exception cref="System.NotImplementedException"></exception>
public IEnumerable<Employee> GetEmployees()
{
    using (ProfilingSession.Current.Step("EmployeeRepository - GetEmployees"))
    {
        List<Employee> employees = new List<Employee>();
 
        string sqlStatement = "select * from Employees order by EmployeeID";
 
        var profiledDbConnection =
            this.DatabaseHelper.GetProfiledConnection(this.ConnectionString, ProfilingSession.Current);
 
        using (var conn = profiledDbConnection)
        {
            conn.Open();
 
            using (var command = conn.CreateCommand())
            {
                command.CommandType = CommandType.Text;
                command.CommandText = sqlStatement;
 
                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        Employee item = new Employee();
 
                        for (int i = 0; i < reader.FieldCount; i++)
                        {
                            PropertyInfo property = item.GetType().GetProperty(reader.GetName(i));
 
                            if (property != null &&
                                !reader.GetValue(i).Equals(DBNull.Value))
                            {
                                ReflectionHelper.SetValue(property.Name, reader.GetValue(i), item);
                            }
                        }
                        employees.Add(item);
                    }
                }
            }
        }
 
        return employees;
    }
}

 

多執行幾次 API 然後開啟瀏覽器並且開啟 http:// localhost:xxxx/api/nanoprofiler/view 頁面

image

點開幾個監測記錄

image

image

image

image

可以看到有下 ProfilingSession 的程式都會有監測記錄,一個方法裡也不限定只能使用一次,可以有多個程式片段去使用 ProfilingSession 包起來,或者是巢狀的方式去做監控都是可以的。

 

不過大家看到上面的 Profiling Result Detail 會不會覺得少了些什麼呢?

不是說可以針對資料存取處理的 T-SQL 也能夠記錄嗎?為何在上面的幾張圖片裡卻什麼都看不到哩。

關於這個,我必須說我也很驚訝,不過要老實說的是我平常所開發的專案並不會直接使用 ADO.NET 來操作資料存取,那種自己有做什麼 SQLHelper 的將一堆 ADO.NET 操作給做成很多方法重複用的,其實我也很少會用,我都是使用 Dapper 居多,而且使用 Dapper 的專案的確是可以將執行的 T-SQL 給記錄起來,但是直接使用 ADO.NET 的操作卻無法記錄到 T-SQL 內容,我還真的不知道問題出在哪裡了?

或許要發個 issue 給作者,請作者提供協助。不過這點我倒是沒那麼急迫,因為前面有說到,我是使用 Dapper 來操作資料存取,而 NanoProfiler.Data 是可以正常的記錄到 Dapper 操作的 T-SQL 內容,所以就這樣啦。

 


上面的收尾有點七零八落的,不過這是現實,因為我不會用的的情境所發生的問題就不是我會想要解決的,我還一堆別的事情等著要做,也因為這篇最後的意外結果,所以就趕緊來寫出使用 Dapper 與 NanoProfiler 的下一篇文章。

 

參考資料

NanoProfiler Wiki - 4. How to enable DB profiling?

NanoProfiler - 适合生产环境的性能监控类库 之 基本功能篇 - Teddy's Knowledge Base - 博客园

 

以上

 

純粹是在寫興趣的,用寫程式、寫文章來抒解工作壓力