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,我想你会喜欢它的。

posted on 2012-10-03 20:14  Fish Li  阅读(20039)  评论(56编辑  收藏