ClownFish 使用說明


ClownFish 是什么?
ClownFish 能做什么?
ClownFish 有什么特色?
如果您有這些疑問,那么請瀏覽ClownFish的介紹博客:ClownFish:比手寫代碼還快的通用數據訪問層

ClownFish適合我嗎?

我認為,對於數據庫的訪問方式目前有以下5種方案:
1. 有些人喜歡使用存儲過程。
2. 有些人不喜歡存儲過程也不喜歡把SQL語句放在C#代碼中。
3. 有些人會在C#中嵌入參數化的SQL語句。
4. 有些人就是喜歡在C#代碼中拼接SQL語句。
5. 還有些人不寫SQL語句而在使用ORM工具。
當然了,還有些人同時混合使用多種方案。
我不知道您屬於哪一類,如果是最后一類,那么我只能說:ClownFish不適合你。
除此之外,ClownFish完全可以滿足您的需要。

注冊數據庫連接字符串

ClownFish是一個通用數據訪問層,它的設計目標就是對多數據庫的支持。下面這些場景都是支持的:
1. 我的項目只訪問一個SQLSERVER的數據庫。
2. 我的項目中需要訪問多個SQLSERVER的數據庫,而且可能是多個SQLSERVER實例。
3. 我的項目中既有SQLSERVER數據庫,還有MySQL數據庫,甚至還包含Access,SQLite …………

好吧,這些這樣要求並不高,ClownFish都可以支持,但是你要告訴ClownFish如何連接它們, 即:注冊數據庫連接字符串。

在ClownFishDEMO中這樣一段配置:

<connectionStrings>
    <clear/>
    <add name="MyNorthwind_MSSQL"
        connectionString="server=localhost\sqlexpress;database=MyNorthwind;Integrated Security=SSPI;"
        providerName="System.Data.SqlClient"/>
    
    <add name="MyNorthwind_MySql" 
         connectionString="server=127.0.0.1;database=MyNorthwind;uid=root;pwd=fish;" 
         providerName="MySql.Data.MySqlClient"/>

    <add name="MyNorthwind_Access"
         connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0};Persist Security Info=True"
         providerName="System.Data.OleDb"/>

    <add name="MyNorthwind_SQLite"
         connectionString="Data Source={0}"
         providerName="System.Data.SQLite"/>
</connectionStrings>

這段配置包含4個數據庫的連接方法與數據提供程序的定義。
下面來看一下,如何給ClownFish注冊數據庫連接字符串。

// 注冊SQLSERVER數據庫連接字符串
ConnectionStringSettings setting = ConfigurationManager.ConnectionStrings["MyNorthwind_MSSQL"];
DbContext.RegisterDbConnectionInfo("sqlserver", setting.ProviderName, "@", setting.ConnectionString);


// 注冊MySql數據庫連接字符串(存儲過程)
ConnectionStringSettings mysql = ConfigurationManager.ConnectionStrings["MyNorthwind_MySql"];
DbContext.RegisterDbConnectionInfo("mysql", mysql.ProviderName, "_", mysql.ConnectionString);


// 注冊Access數據庫連接字符串。
ConnectionStringSettings access = ConfigurationManager.ConnectionStrings["MyNorthwind_Access"];
string mdbPath = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\MyNorthwind.mdb");
string mdbConnectionString = string.Format(access.ConnectionString, mdbPath);
DbContext.RegisterDbConnectionInfo("access", access.ProviderName, string.Empty, mdbConnectionString);


// 注冊SQLite數據庫連接字符串。
ConnectionStringSettings sqlite = ConfigurationManager.ConnectionStrings["MyNorthwind_SQLite"];
string db3Path = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\MyNorthwind.db3");
string sqliteConnectionString = string.Format(sqlite.ConnectionString, db3Path);
DbContext.RegisterDbConnectionInfo("sqlite", sqlite.ProviderName, "@", sqliteConnectionString);

請注意RegisterDbConnectionInfo的第3個參數,它表示【命令參數名稱前綴】。
在SQLSERVER中,要求命令參數的名稱以@開頭,SQLite也支持這個前綴,然而其它數據庫(或者數據驅動程序)不要求這樣的前綴(或者使用其它的符號), 因此,為了不讓這些規定影響我們的項目代碼,ClownFish設計了【命令參數名稱前綴】的方式來處理這類不兼容問題。 由於連接字符串肯定是與數據庫類型相關的,所以,在調用RegisterDbConnectionInfo時,要同時指定【命令參數名稱前綴】這個參數。

根據示例代碼,我們應該可以發現,只需要調用DbContext.RegisterDbConnectionInfo()就可以注冊數據庫連接字符串了。 RegisterDbConnectionInfo有二個重載的方法,有關RegisterDbConnectionInfo的方法說明可以參考《ClownFish-API-Documentation.chm》

看過了示例代碼,再來回顧一下如何為那3種場景注冊數據庫連接字符串。

1. 我的項目只訪問一個SQLSERVER的數據庫。
此時只需要調用一次RegisterDbConnectionInfo()即可,這種場景很簡單。

2. 我的項目中需要訪問多個SQLSERVER的數據庫,而且可能是多個SQLSERVER實例。
此時你有三個可選方案:
a. 為每個數據庫調用一次RegisterDbConnectionInfo(),並給出相應的連接字符串。
b. 只調用一次RegisterDbConnectionInfo(),但是在實例化DbContext時給出具體的連接字符串。
c. 只調用一次RegisterDbConnectionInfo(),並給obtainFunc參數賦值一個委托,由委托在運行時返回具體的連接字符串。

3. 我的項目中既有SQLSERVER數據庫,還有MySQL數據庫,甚至還包含Access,SQLite …………
前面的示例代碼就已經演示了這種場景,請參考那些代碼。

說明:ClownFish不去讀取配置文件,需要調用方自行實現保存連接字符串的方式。

ClownFish對實體類型的要求

ClownFish支持將數據庫返回的結果集轉換成實體對象或者實體列表,也支持根據實體對象給參數化SQL填充輸入參數。 然而,ClownFish對實體的類型定義沒有任何要求。

在某些特殊情況下,
1. 可能你的實體中的數據成員名稱與數據庫的字段名稱不一樣,那么就需要設置映射關系。
2. 某個數據成員的值明確不需要從數據庫中加載。
3. 實體有嵌套關系,數據庫返回的結果使用前綴來區分嵌套關系。
在這些情況下,你就需要使用DbColumnAttribute來明確告訴ClownFish該如何處理這些情況。

DbColumnAttribute的定義如下:

/// <summary>
/// 用於標識實體的每個數據成員的一些加載信息
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, 
            AllowMultiple = false, Inherited = false)]
public sealed class DbColumnAttribute : Attribute
{
    /// <summary>
    /// <para>在加載數據時,不加載這個成員。</para>
    /// <para>注意:如果數據結果中不包含匹配的字段,對應的數據成員也不會被加載。</para>
    /// </summary>
    public bool IgnoreLoad { get; set; }


    /// <summary>
    /// 數據庫中對應的字段名,如不指定,則與成員的名稱相同。
    /// </summary>
    public string Alias { get; set; }


    /// <summary>
    /// <para>指示加載嵌套類型時,所有子類型的成員必須以什么字符串做為前綴。</para>
    /// <para>string.Empty or null 表示沒有前綴。</para>
    /// <para>【*】 表示以【成員名字.】做為前綴</para>
    /// </summary>
    public string SubItemPrefix { get; set; }

代碼中的注釋已經解釋了這些屬性的用途,這里不再多說。

注意:DbColumnAttribute只是用於修飾實體的數據成員的。實體的類型定義,ClownFish不作限制。

讓ClownFish以編譯模式工作

為了能讓ClownFish在運行時擁有最優秀的性能,ClownFish采用了動態生成代碼並編譯的方案, 而且,為了避免在生成代碼、編譯代碼期間對調用線程的阻塞影響, ClownFish采用了后台線程來處理這類任務,如果要加載的實體類型的加載器沒有編譯完成, 會按照老版本的方式直接采用反射的方式來執行,一旦加載器編譯完成,則后續調用將會使用加載器。

所以,這里又涉及到加載器的生成及和編譯時機問題。

在編譯時機這個問題上,我更關注的是性能影響,所以設計了編譯模式的概念來解決。
而且,為了保證ClownFish能滿足各種使用場景,ClownFish提供了三種編譯方法:

public static class BuildManager
{
    // 自動編譯模式,此模式會自動收集待編譯的數據實體類型。
    public static void StartAutoCompile(Func<bool> func)
    public static void StartAutoCompile(Func<bool> func, int timerPeriod)


    // 手動提交編譯模式,此模式要求手動提交需要編譯的數據實體類型。
    public static void CompileModelTypesSync(Type[] types, bool throwOnFailure)
    public static void CompileModelTypesAsync(Type[] types)
}

【手動提交編譯模式】適合在程序初始化時調用,它又分為【同步】提交和【異步】提交二種方式。
如果所有的數據實體類型有一定特征,那么可以考慮這種方式。調用方法:

Type[] models = BuildManager.FindModelTypesFromCurrentApplication(
                            t => t.FullName.StartsWith("Test.Models."));
BuildManager.CompileModelTypesSync(models, true);

或者調用異步版本:

Type[] models = BuildManager.FindModelTypesFromCurrentApplication(
                            t => t.FullName.StartsWith("Test.Models."));
BuildManager.CompileModelTypesAsync(models);

在示例代碼中,由於所有的數據實體類型都定義在 Test.Models 命名空間中, 所以只需二行代碼就可以為所有的實體類型生成加載器供運行時調用。

【自動編譯模式】的啟動也很簡單:

// 啟動自動編譯數據實體加載器的工作模式。
// 編譯的觸發條件:請求實體加載器超過2000次,或者,等待編譯的類型數量超過100次
BuildManager.StartAutoCompile(() => BuildManager.RequestCount > 2000 || BuildManager.WaitTypesCount > 100);

// 啟動自動編譯數據實體加載器的工作模式。每10秒【固定】啟動一個編譯過程。
// 注意:StartAutoCompile只能調用一次,第二次調用時,會引發異常。
//BuildManager.StartAutoCompile(() => true, 10000);

說明:
1. 手工提交編譯的方法可以多次調用,每次調用都可能會生成一個程序集。
2. 手工提交編譯模式與自動編譯模式可以混用。
3. 自動編譯模式的方法只能調用一次。

說到這里,恐怕有人會問:到底是該選擇手動提交編譯模式還是自動編譯模式?
答:手動提交編譯模式要求傳入Type[]類型的參數值,如果你能很容易找出所有實體類型,那么,建議選擇手動提交編譯模式, 相反,如果實體類型完全沒有規律,很難把它們找出來,那么就只能使用自動編譯模式了。

對於自動編譯模式和異步提交編譯模式來說,所有代碼都是由后台線程處理的,如果在處理期間發生了異常, 不能直接通知工作線程,因此,ClownFish提供了一個事件用於通知編譯異常,示例代碼如下:

public static void Init()
{
    // 注冊編譯失敗事件,用於檢查在編譯實體加載器時有沒有失敗。
    BuildManager.OnBuildException += new BuildExceptionHandler(BuildManager_OnBuildException);

    // .............................................
}

static void BuildManager_OnBuildException(Exception ex)
{
    CompileException ce = ex as CompileException;
    if( ce != null )
        SafeLogException(ce.GetDetailMessages());
    else
        // 未知的異常類型
        SafeLogException(ex.ToString());
}

由於ClownFish實現了二種代碼運行路徑:編譯與反射方案並存。 考慮到編譯模式又有三種工作方式,因此,ClownFish在默認情況下,並不啟用編譯模式,而是使用反射方案。 如果需要提高性能,那么必須通過明確調用讓ClownFish進入編譯模式。

我認為ClownFish的性能絕對是優秀的,如果您將ClownFish與其它的數據訪問層做性能測試比較時, 發現其它數據訪問層比ClownFish快幾倍,那么請不要驚喜或者認為ClownFish很差勁。 請檢查一下ClownFish是否工作在編譯模式下! 也希望 http://www.cnblogs.com/humble/archive/2012/08/20/2647286.html 這樣的鬧劇不要再次發生。

ClownFish可以生成非常優秀的通用代碼,而且基於編譯的方式,因此,想在性能上超過ClownFish,可能性非常渺茫。

ClownFish 支持的數據庫訪問方式

ClownFish是一個不支持動態生成SQL的數據訪問層,因此,所有的數據庫操作,你必須提供SQL語句。 在這個背景下,您可以自行決定如何組織您的SQL語句。

我認為,您的SQL語句在項目中只可能以4種形式出現:
1. 拼接非參數化SQL
2. 拼接(或者定義)參數化SQL
3. 存儲過程
4. 保存在XML文件中的參數化SQL

為了區分以上這些形式,ClownFish中專門定義了一個枚舉:

/// <summary>
/// 表示要執行什么類型的命令
/// </summary>
public enum CommandKind
{
    /// <summary>
    /// 表示要執行一個存儲過程或者是一個XmlCommand
    /// </summary>
    SpOrXml,
    /// <summary>
    /// 表示要執行一個存儲過程
    /// </summary>
    StoreProcedure,
    /// <summary>
    /// 表示要執行一個XmlCommand
    /// </summary>
    XmlCommand,
    /// <summary>
    /// 表示要執行一條沒有參數的SQL語句
    /// </summary>
    SqlTextNoParams,
    /// <summary>
    /// 表示要執行一條包含參數的SQL語句
    /// </summary>
    SqlTextWithParams
}

在這4類訪問數據庫的方式中,我極不推薦第一種方法,太不安全了。 但是考慮到每個人的追求(或者喜好)不同,ClownFish還是支持這種方式(畢竟實現難度最低)。

示例代碼:

List<Product> products = DbHelper.FillList<Product>(
        "select * from Products order by ProductId", null, access, CommandKind.SqlTextNoParams);



對於第二,三種方式(參數化SQL與存儲過程),我想大家都懂,它們是安全的代表。
注意哦:如果在存儲過程中拼接SQL,那么仍然是不安全的。

示例代碼:

Product p2 = DbHelper.GetDataItem<Product>("GetProductById", new { ProductID = 5 });
            
List<Product> list = DbHelper.FillList<Product>(
        "select * from Products where CategoryId = @CategoryId",
        new { CategoryId = 3 }, dbContext, CommandKind.SqlTextWithParams);



對於第四種方式(保存在XML文件中的參數化SQL),ClownFish稱它為XmlCommand, 關於XmlCommand的介紹請點擊這里

示例代碼:

DbHelper.ExecuteNonQuery("UpdateProduct", p2);

ClownFish中,對XmlCommand與存儲過程有着同樣好的支持,而且調用基本是一樣的,因此,DbHelper.DefaultCommandKind的默認值是SpOrXml, 如果在調用DbHelper時沒有給出CommandKind,那么就會使用DbHelper.DefaultCommandKind做為默認值。

ClownFish對CPQuery的支持

在前面的小節中,我已表達過我的觀點:我是非常反對【拼接SQL】的。 對於一些需要動態查詢條件的場合,我認為可以使用【拼接參數化SQL】的方式來解決, 然而,【拼接SQL】是一種非常入門級的方法,現在已深入人心,許多人已經形成了習慣, 因此,很難改變這種壞習慣,尤其是在一些項目中,【拼接SQL】的代碼已大量存在了,這也是非常現實的問題。 為了解決這個問題,我設計了CPQuery,它能使用大家都習慣的【加號】拼接方式來拼接參數化SQL, 而且,改造現有的【拼接SQL】為【參數化SQL】的過程也非常簡單。 有關CPQuery的介紹與實現原理,請參考:CPQuery, 解決拼接SQL的新方法

設計CPQuery有2個主要目標:
1. 提供一種簡單的方法,用於改造現有的拼接SQL為參數化SQL
2. 保留大家都熟悉的習慣,繼續用【加號】的方式來寫參數化SQL
因此,CPQuery不提供數據訪問功能,所以它必須要與其它數據訪問層配合使用。

ClownFish現在已經集成了CPQuery,因此,完全可以使用在ClownFish使用CPQuery來替代以前的拼接SQL方法。

為了方便的使用CPQueryClownFish的DbHelper類為所有的數據庫訪問方法提供了對應的擴展方法:

public static int ExecuteNonQuery(this CPQuery query)
public static int ExecuteNonQuery(this CPQuery query, DbContext dbContext)
public static object ExecuteScalar(this CPQuery query)
public static object ExecuteScalar(this CPQuery query, DbContext dbContext)
public static T ExecuteScalar<T>(this CPQuery query)
public static T ExecuteScalar<T>(this CPQuery query, DbContext dbContext)
public static T GetDataItem<T>(this CPQuery query) where T : class, new()
public static T GetDataItem<T>(this CPQuery query, DbContext dbContext) where T : class, new()
public static List<T> FillList<T>(this CPQuery query) where T : class, new()
public static List<T> FillList<T>(this CPQuery query, DbContext dbContext) where T : class, new()
public static List<T> FillScalarList<T>(this CPQuery query)
public static List<T> FillScalarList<T>(this CPQuery query, DbContext dbContext)
public static DataTable FillDataTable(this CPQuery query)
public static DataTable FillDataTable(this CPQuery query, DbContext dbContext)

所以,使用起來也非常容易:

// 創建動態查詢
var query = BuildDynamicQuery(p);

DataTable table = query.FillDataTable();

如果想知道如何使用CPQuery來拼接參數化SQL,請參考:CPQuery, 解決拼接SQL的新方法

DbHelper:簡化對數據庫的訪問

前面的許多示例代碼都使用了DbHelper提供的功能,這個小節再來介紹DbHelper到底提供了哪些功能。

DbHelper在內部主要是封裝了對DbContext的操作,提供一個簡潔的API供外部使用。
它提供了下面這些數據庫訪問的方法:

public static int ExecuteNonQuery(  /* 6 overloads */  );
public static object ExecuteScalar(  /* 6 overloads */  );
public static T ExecuteScalar<T>(  /* 6 overloads */  );
public static List<T> FillScalarList<T>(  /* 6 overloads */  );
public static DataTable FillDataTable(  /* 6 overloads */  );
public static List<T> FillList<T>(  /* 6 overloads */  ) where T : class, new();
public static List<T> FillListPaged<T>(  /* 4 overloads */  ) where T : class, new();
public static T GetDataItem<T>(  /* 6 overloads */  ) where T : class, new();

每個數據訪問方法都提供這樣6個重載版本:

public static int ExecuteNonQuery(this CPQuery query);
public static int ExecuteNonQuery(this CPQuery query, DbContext dbContext);
public static int ExecuteNonQuery(string nameOrSql, object inputParams);
public static int ExecuteNonQuery(string nameOrSql, object inputParams, CommandKind cmdKind);
public static int ExecuteNonQuery(string nameOrSql, object inputParams, DbContext dbContext);
public static int ExecuteNonQuery(string nameOrSql, object inputParams, DbContext dbContext, CommandKind cmdKind);

關於這些方法的功能,以及參數說明,請參考《ClownFish-API-Documentation.chm》。

有些DbHelper的數據訪問方法,不要求提供DbContext的實例,那么當需要DbContext的實例時,DbHelper會調用它定義的CreateDefaultDbContext委托:

//     創建默認DbContext實例的方法。
//     用於在調用DbHelper的某些重載方法時沒有傳入DbContext對象。
//     默認實現方式是:根據第一個注冊連接信息創建一個不使用事務的DbContext實例。
//     此委托類型的輸入參數是在即將要執行的SQL語句、XmlCommand名稱或者存儲過程名稱
public static Func<string, DbContext> CreateDefaultDbContext { get; set; }

如果你認為這種默認實現方法不適合你的項目,那么請提供自己的實現方法。
說明:委托的輸入參數為將要執行的存儲過程名稱,或者XmlCommand名稱,或者SQL語句。

DbHelper還提供了二個從XML文件中加載實體對象的方法:

public static List<T> FillListFromTable<T>(DataTable table) where T : class, new();
public static List<T> FillListFromXmlFile<T>(string xmlPath) where T : class, new();

DbContext:執行數據庫操作的核心類型

ClownFish是一個建立在ADO.NET基礎上的通用數據訪問層, ClownFish在內部包裝了Connection, Transaction, Command, Parameter這些與數據庫訪問相關的對象, 這些內部封裝由DbContext來實現,因此,對於ClownFish來說,它是執行數據庫操作的核心類型。

如果之前介紹的DbHelper不能滿足功能需要,那么可以直接使用DbContext,或者可以實現自己的對DbContext的包裝方法。 相比DbHelper而言,DbContext允許更多地控制數據訪問細節, 比如:DbContext允許直接訪問Connection, Transaction, Command來實現DbHelper不提供的功能。

DbHelper的定位是一個工具類,因此它是一個靜態類,然而,ClownFish是一個允許實例化的類型,它有以下構造方法:

/// <summary>
/// 構造方法
/// </summary>
/// <param name="useTransaction">是否使用事務</param>
public DbContext(bool useTransaction)

/// <summary>
/// 構造方法
/// </summary>
/// <param name="configName">調用RegisterDataBaseConnectionInfo()時指定的配置名稱</param>
public DbContext(string configName)

/// <summary>
/// 構造方法
/// </summary>
/// <param name="configName">調用RegisterDataBaseConnectionInfo()時指定的配置名稱</param>
/// <param name="useTransaction">是否使用事務</param>
public DbContext(string configName, bool useTransaction)

/// <summary>
/// 構造方法,可以指定連接哪種數據庫,以及連接字符串和是否使用事務。
/// </summary>
/// <param name="configName">調用RegisterDataBaseConnectionInfo()時指定的配置名稱</param>
/// <param name="connectionString">連接字符串,如果為空,則使用默認的連接字符串</param>
/// <param name="useTransaction">是否使用事務</param>
public DbContext(string configName, string connectionString, bool useTransaction)

注意:
1. 一定要先調用過RegisterDataBaseConnectionInfo,才能實例化DbContext對象。
2. 雖然RegisterDataBaseConnectionInfo方法允許指定連接字符串,但是這里允許重新指定。
3. 如果不指定configName,那么使用第一次調用RegisterDataBaseConnectionInfo時提供的名稱。

ClownFish提供以下三個屬性,給你最后控制ADO.NET的機會:

/// <summary>
/// 當前連接對象
/// </summary>
public DbConnection Connection
{
    get { return _conn; }
}

/// <summary>
/// 當前事務對象
/// </summary>
public DbTransaction Transaction
{
    get { return _trans; }
}

/// <summary>
/// 當前的命令對象,每當執行數據庫的操作時,都會在這個對象上執行。
/// </summary>
public DbCommand CurrentCommand
{
    get { return _command; }
}

同時提供以下方法供你控制命令參數:

public void AddParameterWithValue(string paraName, object paraValue)
public DbParameter AddParameter(string paraName, object paraValue, DbType paraType)
public DbParameter AddParameter(string paraName, object paraValue, DbType paraType, int size)
public DbParameter AddParameter(string paraName, object paraValue, DbType paraType, int? size, ParameterDirection inout)

如果您要調用的存儲過程或者參數化SQL包含了較多的命令參數,DbHelper有個工具方法可以簡化為所有參數賦值的任務:

/// <summary>
/// 根據一個數據實體實例,自動設置DbContext的“當前命令”的參數
/// </summary>
/// <param name="dbContext">DbContext實例</param>
/// <param name="inputParams">包含所有命令參數的數據對象</param>
public static void SetCommandParameters(this DbContext dbContext, object inputParams)

還是繼續說ADO.NET的命令對象,DbContext提供下面二個方法允許你創建命令對象:

/// <summary>
/// 根據【存儲過程名稱或SQL語句】,在當前連接上下文中創建命令對象
/// </summary>
/// <param name="sqlOrName">存儲過程名稱或者SQL語句</param>
/// <param name="commandType">命令類型</param>
/// <returns>創建的命令對象</returns>
public DbCommand CreateCommand(string sqlOrName, CommandType commandType)


/// <summary>
/// 根據配置文件中【命令名】,在當前連接上下文中創建命令對象
/// </summary>
/// <param name="commandName">XmlCommand的名字</param>
/// <returns>創建的命令對象</returns>
public DbCommand CreateXmlCommand(string commandName)

完整的示例代碼可參考:

public bool InsertProduct(Product product)
{
    // 創建命令
    this.DbContext.CreateXmlCommand("InsertProduct");
    // 設置當前命令中的所有參數
    this.DbContext.SetCommandParameters(product);
    // 調用存儲過程
    return (this.DbContext.ExecuteNonQuery() > 0);
}

public bool DeleteProduct(int productId)
{
    // 創建命令
    this.DbContext.CreateXmlCommand("DeleteProduct");

    //// 設置當前命令中的參數
    //this.DbContext.GetCommandParameter("ProductID").Value = productId;

    // 或者下面的方法也是可以的。
    this.DbContext.SetCommandParameters(new { ProductID = productId });

    // 調用存儲過程
    return (this.DbContext.ExecuteNonQuery() > 0);
}

DbContext提供下面這些用於執行數據庫操作的方法(DbHelper的數據庫訪問功能全是下面這些方法的包裝版本):

public int ExecuteNonQuery()
public object ExecuteScalar()
public List<T> FillScalarList<T>()
public DataTable FillDataTable()
public DataSet FillDataSet(params string[] tableNames)
public List<T> FillList<T>() where T : class, new()
public T GetDataItem<T>() where T : class, new()

注意:DbContext實現了IDisposable接口,會在Dispose中正確關閉連接,因此,如果要直接使用DbContext,推薦采用using的使用方式:

using( DbContext dbContext = new DbContext(false) ) {
    // .................................
    // 執行你的數據庫調用
    // .................................
}

在一個數據庫連接中執行多個命令

DbHelper提供的靜態方法可以讓我們只需要一行代碼就能完成整個數據訪問過程。 然而,那些只需一行代碼的背后卻包含了打開與關閉數據連接的功能。 當執行多次調用時,每個操作其實都需要打開連接。

在一個連接中執行多次數據操作是個很常見的要求:
1. 為了讓多次操能做為一個事務運行。
2. 避免頻繁打開關閉連接帶來的性能影響。

如果你也有這樣的要求,在一個數據庫連接中執行多個命令,那么ClownFish為你提供了三種可供選擇的方案。

1. 直接使用DbContext,示例代碼如下:

using( DbContext dbContext = new DbContext(false) ) {
    dbContext.CreateCommand("insert into Customers ......", CommandType.Text);
    dbContext.SetCommandParameters(product);
    dbContext.ExecuteNonQuery();

    
    dbContext.CreateCommand("insert into Products ......", CommandType.Text);
    dbContext.SetCommandParameters(customer);
    dbContext.ExecuteNonQuery();
}

2. 仍然使用DbHelper,示例代碼如下:

Product product = CreateTestProduct();

using( DbContext mysql = new DbContext("mysql-xcmd") ) {
    // 插入一條新記錄
    product.ProductID = DbHelper.ExecuteScalar<int>("InsertProduct_MySqlCmd", product, mysql);

    // 查詢新插入的記錄
    Product p2 = DbHelper.GetDataItem<Product>("GetProductById_MySqlCmd", new { ProductID = product.ProductID }, mysql);

    // 更新記錄
    p2.ProductName = "New ProductName.";
    DbHelper.ExecuteNonQuery("UpdateProduct_MySqlCmd", p2, mysql);

    // 獲取更新后的記錄
    Product p3 = DbHelper.GetDataItem<Product>("GetProductById_MySqlCmd", new { ProductID = p2.ProductID }, mysql);
    
    // 刪除記錄
    DbHelper.ExecuteNonQuery("DeleteProduct_MySqlCmd", new { ProductID = product.ProductID }, mysql);

    List<Product> list = DbHelper.FillList<Product>("GetProductByCategoryId_MySqlCmd", new { CategoryID = 1 }, mysql);
}

3. 使用DbContextHolderBase,后面再說。

ClownFish 對事務的支持

ClownFish中使用事務非常簡單,只需要實例化DbContext時,給useTransaction參數賦值true即可, 然后,就可以執行多個數據庫操作,最后,再調用CommitTransaction()即可。 示例代碼如下:

using( DbContext dbContext = new DbContext(true) ) {
    dbContext.CreateCommand("insert into Customers ......", CommandType.Text);
    dbContext.SetCommandParameters(product);
    dbContext.ExecuteNonQuery();

    
    dbContext.CreateCommand("insert into Products ......", CommandType.Text);
    dbContext.SetCommandParameters(customer);
    dbContext.ExecuteNonQuery();
    
    dbContext.CommitTransaction();
}

或者:

using( DbContext dbContext = new DbContext(true) ) {
    DbHelper.ExecuteNonQuery("insert into Customers ......", 
                product, dbContext, CommandKind.SqlTextWithParams);

    DbHelper.ExecuteNonQuery("insert into Products ......", 
                customer, dbContext, CommandKind.SqlTextWithParams);

    dbContext.CommitTransaction();
}

說明:如果調用過程中發生異常,ClownFish負責回滾事務。

ClownFish 對“多帳套數據庫”的支持

“多帳套數據庫”也是一種常見的業務需求,在這種場景下,數據庫有多個,而且結構完全相同, 程序在運行時切換到登錄人選擇的數據庫中執行操作。

ClownFish的設計基礎就是對多數據庫支持,因此,實現多帳套數據庫是非常容易的。 另外,由於帳套是由用戶創建的,所以,數據庫的個數並不是固定的。 在這種情況下,不能通過多次調用RegisterDataBaseConnectionInfo的方法來解決。 但是,DbContext的構造函數允許傳入連接字符串。 因此,可以寫一個方法專門負責創建相應的DbContext實例,然后傳遞給DbHelper的那些數據訪問方法,例如:

using( DbContext dbContext = CreateDbContextInstance() ) {
    DbHelper.ExecuteNonQuery( ............ , inputParams, dbContext);
}

在自己實現的CreateDbContextInstance方法中,你需要根據當前用戶得到數據庫連接字符串,進而創建DbContext實例。

為了讓調用代碼能夠更加簡單,DbContext.RegisterDbConnectionInfo方法提供一個重載版本,允許傳入一個獲取連接字符串的委托:

/// <summary>
/// <para>注冊數據庫的連接信息。</para>
/// <para>如果程序要訪問二種不同類型的數據庫,如:SQLSERVER和MySql,
///        那么至少需要調用本方法二次。</para>
/// <para>每種類型的數據庫如果有多個“數據庫的連接”,可以在構造方法中指定。
///        這里的連接字符串只是做為默認的連接字符串</para>
/// </summary>
/// <param name="configName">配置名稱:不同種類的數據庫的配置名稱,
///        如:MSSQL, MySql。這個參數用於后續調用時傳入構造方法中</param>
/// <param name="providerName">數據提供者名稱</param>
/// <param name="cmdParamNamePrefix">命名參數的名稱前綴。</param>
/// <param name="obtainFunc">獲取連接字符串的委托,僅當在不指定連接字符串時使用</param>
public static void RegisterDbConnectionInfo(string configName,
        string providerName, string cmdParamNamePrefix, Func<string, string> obtainFunc)

說明:obtainFunc委托的輸入參數是與之匹配的configName

具體使用方法,可參考下面的演示代碼:

public static void Init()
{
    // 注冊Access數據庫連接字符串。
    ConnectionStringSettings access = ConfigurationManager.ConnectionStrings["MyNorthwind_Access"];

    // 注意最后一個參數,它是一個委托,用於在需要連接字符串時返回一個連接字符串。
    DbContext.RegisterDbConnectionInfo("access", access.ProviderName, string.Empty, ObtainConnectionString);
    s_connectionStringFormat = access.ConnectionString;
}


private static string ObtainConnectionString(string configname)
{
    if( HttpContext.Current == null )
        throw new InvalidOperationException();

    // 注意:在Global.asax中,我已訂閱了Application_PostResolveRequestCache事件,
    //        會調用GetAccountNameFormRequest()從Cookie中提取帳套數據庫的名稱,放在HttpContext中。
    string accountDb = HttpContext.Current.Items[STR_AccountDbKey] as string;

    string mdbPath = Path.Combine(HttpRuntime.AppDomainAppPath, "App_Data\\" + accountDb + ".mdb");
    return string.Format(s_connectionStringFormat, mdbPath);
}

經過這樣的初始化調用后,程序中其它地方就和操作單一數據庫是一模一樣了。

ClownFish 對日志與監視的支持

我想很多人都用過 SQL SERVER Profiler 這樣的工具,它可以讓我們方便地觀察應用程序對數據庫的訪問情況。 然而,ClownFish的設計目標並不只是支持SQLSERVER,而是支持所有ADO.NET能夠支持的數據庫。 不過,並不是每個數據庫都提供類似 SQL SERVER Profiler 這樣強大的工具, 為了讓用戶能夠繼續方便地觀察應用程序對數據庫的訪問情況,尤其是方便開發人員在本機觀察, ClownFish也提供了一個類似的工具,ClownFishSQLProfiler

由於ClownFish是個數據訪問層,因此,它知道每次執行數據庫操作的所有信息, 並且,DbContext在它執行數據庫操作的前后都提供了足夠的事件, 所以,只要訂閱這些事件,你也能開發自己的Profiler工具(基於ClownFish)。

/// <summary>
/// DbContext在執行數據庫操作時發生異常時引發的事件,供記錄日志使用。
/// </summary>
public static event DbContextExceptionHandler OnException;

/// <summary>
/// 每次在執行數據庫操作前會觸發的事件。可用此事件記錄程序執行了哪些操作。
/// </summary>
public static event DbContextEventHandler OnBeforeExecute;

/// <summary>
/// 每次在執行數據庫操作完成時會觸發的事件。
/// </summary>
public static event DbContextEventHandler OnAfterExecute;

/// <summary>
/// 每次打開數據庫連接時會觸發的事件
/// </summary>
public static event DbContextEventHandler OnOpenConnection;

ClownFish的下載壓縮包中,ClownFishSQLProfiler.exe是一個單獨的EXE程序。 被監視的網站需要在初始化時調用Profiler.TryStartClownFishProfiler()來啟動監視線程,完整的代碼如下:

// 開始准備向ClownFishSQLProfiler發送所有的數據庫訪問操作日志
Profiler.ApplicationName = "ClownFishDEMO";
Profiler.TryStartClownFishProfiler();

為了能保證監視能正常工作,還需要確保網站bin目錄下存在ClownFishProfilerLib.dll文件, 此文件訂閱了前面所說的事件,並通過Remoting給ClownFishSQLProfiler.exe發送應用程序訪問數據庫的所有操作細節。 當ClownFishProfilerLib.dll不存在時,ClownFishSQLProfiler.exe不會收到任何通知,但並不影響網站正常運行。

有人問過我:為什么不把ClownFishProfilerLib.dll的功能集成到ClownFish中?
答:
1. Profiler功能並不是每個程序必需的,或者,我期待有人實現他自己的Profiler工具。
2. 單獨提供ClownFishProfilerLib.dll的好處是,把這個文件放進bin目錄,就可以啟動監視功能,把它刪除就可以停止監視功能,完全不用修改一行代碼,我認為使用會比較方便。

DbContext提供的這些事件,並不僅僅為了實現Profiler工具,還允許你將所有的數據庫訪問細節記錄到日志文件中。

說明:如果想知道這些事件是否對應一個DbContext對象,那么可以給DbContext的Tag屬性指定一個自定義的對象,供后面的事件中判斷。

ClownFish 的一些全局設置

前面已經介紹了DbHelper的DefaultCommandKind,CreateDefaultDbContext這二個重要的全局變量(靜態屬性), 這里再來介紹ClownFish的其它一些全局設置。

DbContext有個AutoRetrieveOutputValues屬性用於控制在調用存儲過程后,是否需要讀出輸出參數:

/// <summary>
/// 對於當前實例,是否要在調用存儲過程完成后,自動獲取輸出的參數值。默認值:false
/// </summary>
public bool AutoRetrieveOutputValues { get; set; }

如果您希望每次調用存儲過程后,都能取回輸出參數的值,寫回到實體的對應屬性(或者字段),顯然每次去設置這個實例屬性就不夠方便了, 那么,您可以設置靜態屬性:DbContextDefaultSetting.AutoRetrieveOutputValues,它是DbContext.AutoRetrieveOutputValues的默認值。

DbContextDefaultSetting還提供另一個可讀寫的靜態屬性:ListResultCapacity

/// <summary>
/// <para>當從數據庫中返回一個實體列表時,為列表的初始化長度是多少。默認值:50;</para>
/// <para>對於有分頁的應用程序,請根據程序的分頁大小來合理地設置此參數。</para>
/// </summary>
public static int ListResultCapacity

請根據項目情況,正確設置它,對性能提升會有好處。

如果你還需要對Connection,Command做統一的配置,那么可以使用DbContext的事件。
例如:下面這行代碼能修改所有命令的執行超時時間。


DbContext.OnBeforeExecute += ctx => ctx.CurrentCommand.CommandTimeout = 300;

ClownFish 對分層開發的支持

ClownFish的定位是一個通用數據訪問層,因此,在設計的過程中,一直沒有忘記ClownFish在實際項目中的位置。
考慮到不同項目對數據庫訪問可能會有不同的要求,ClownFish提供二個級別的數據訪問API,
而且還考慮了連接共享等等與分層相關的架構問題。

我認為在一個分層架構的項目中,使用ClownFish的方式有以下幾種方法:
1. 如果數據訪問的要求不高,可以直接在BLL中調用DbHelper提供的數據訪問方法。
2. 如果DbHelper不能滿足要求,那么可以創建自己的DAL,並在其中使用DbContext執行數據訪問操作。
3. 如果希望BLL層共用連接並且使用事務,但又不打算使用高成本的分布式事務,那么可以讓BLL類實現DbContextHolderBase,
   具體實現過程請參考ClownFishDEMO的MyServiceLayer演示代碼。


好了,ClownFish的使用說明就說到這里。如果需要下載ClownFish以及示例代碼,請點擊: ClownFish:比手寫代碼還快的通用數據訪問層

如果你不是一個ORM的狂熱份子,那么歡迎試用ClownFish,我想你會喜歡它的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM