DbHelperV2 - Teddy的通用数据库访问组件设计和思考
一、前言
最近一段时间,总结了自己对数据编程和.Net编程的相关经验,借鉴了各种有益的数据库编程模式,重写了自己半年前写的数据库访问组件(现在看来半年前的版本真是惨不忍睹),新的版本定为DbHelperV2,以下将对该组件的设计思想在做一个分析,一方面 是对自己思路的总结,也希望路过的朋友能为我指出缺失!
二、参考资料
1、数据访问模式——面向对象应用中的数据库交互 [美]Clifton Nock
2、企业应用架构模式(Patterns of Enterprise Application Architecture) (美)Martin Fowler
3、MSDN Library——Reflection
4、Microsoft Pet Shop 3.0 Source Code
三、主要接口
{
Properties
Public Methods
}
四、Sql语句构造
因为各大数据库厂商的不同数据库系统的Sql语句于法有差异,但是最基本的CRUD语句基本一致,因此,建议尽量采用视图来select数据,这样就能都用最基本的,形如
Select [columnlist] from [tablename] where [condition] order by [orderlist]
这样的语句来select数据,这也是StatementFactory的主要作用,另外,利用StatementFactory生成的select语句和下一章介绍的分页接口,就能实现分页语句的自动构造。
/// 类StatementFactory用于创建基本Sql通用CRUD语句,
/// 这些基本语句,都是各大数据库厂商通用的。
/// <summary>
public class StatementFactory
{
public StatementFactory(string tableName);
/// <summary>
/// 利用反射创建插入语句
/// </summary>
public string CreateInsertStatement(Type domainObjType,
params string[] exceptColumns);
/// <summary>
/// 不用反射创建插入语句
/// </summary>
public string CreateInsertStatement(params string[] includeColumns);
/// <summary>
/// 利用反射创建更新语句
/// </summary>
public string CreateUpdateStatement(WhereClause where,
Type domainObjType, params string[] exceptColumns)
/// <summary>
/// 不用反射创建更新语句
/// </summary>
public string CreateUpdateStatement(WhereClause where,
params string[] includeColumns)
public string CreateDeleteStatement(WhereClause where);
}
五、分页查询
比较了各种数据库查询中的分页方法(数据库游标、top...max... 、top... not in...),通用性最强、性能也较好的还是,最后一种,即形如: select top [pagesize] [columns] from [tablename] where [conditions] and [id] not in (select [id] ...) order by [orderlists]的方式进行分页,组件中,为isql server 和access数据库分别实现了IPageSplitter接口,可以由形如select [columns] from [tablename] where [conditions] order by [orderlists]的简单语句,构造上述分页语句,从而实现了分页语不分页查询的无缝衔接。
在实现分页接口的时,还发现Access和Sql Server分页和统计语法的两点差异:
1、 对于from后接内嵌select查询,Access语法可以这样实现:
Select count(*) from (select * from [tablename])
但是,这条语句将会被Sql Server报错,在SqlServer 2000下必须这样写:
Select count(*) from (select * from [tablename]) tmpname
其中,tmpname是为select * from [tablename]的查询结果加一个别名,不加别名,会报语法错误;
2、 对于形如select top 10 [columnlist] … where (… and [id] not in (select top 0 …))这样的语句,注意not in后跟的是top 0,也就是,取第一页数据时的情况,Sql Server 运行没问题,但是Access会报语法错误,因为,Access不允许top后面跟数字0,所以对Access数据库,在分页取第一页数据时,必须区别对待,去掉not in (select top 0)那一段。
/// 基本分页接口如下
/// </summary>
public interface IPageSpliter
{
int PageSize { get; set; }
int GetRowCount();
int GetPageCount();
int GetPageNo();
DataSet GetNextPage();
DataSet GetPage(int pageNo);
bool IsBeforeFirstPage();
bool IsAfterLastPage();
}
六、DbParameter构造
数据库参数的使用,能在很大程度上减少由于Sql注入和不同厂商的数据库差异造成的安全问题和Sql语句语法差异问题,但是对编程人员来说又是一个简单枯燥的过程,因此组建提供了一个DbParameter构造辅助工具接口IDbParameterFactory,和StatementFactory类似,IdbParameterFactory同样提供了利用反射何不用反射的多个构造版本。
/// 构造IDataParameter的通用接口
/// </summary>
public interface IDbParameterFactory
{
/// <summary>
/// 利用反射构造Parameters
/// </summary>
IDataParameter[] CreateParams(object domainObj);
/// <summary>
/// 不用反射构造Parameters
/// </summary>
IDataParameter[] CreateParams(string[] paramNames, object[] paramValues);
/// <summary>
/// 组合Parameters
/// </summary>
IDataParameter[] CombineParams(IDataParameter[] oldParms,
params IDataParameter[] newParms);
/// <summary>
/// 创建单个Parameter
/// </summary>
IDataParameter CreateParam(string name, object val);
}
七、缓存
对象创建的开销、组件的通用性、使用较多反射,在不进行优化的情况下,特别对数据库访问这样可能会在高并发 环境下运转的系统组件,将极大的影响性能,缓存的充分使用,可以在很大程度上缓解这些问题,在本组件中,以下几处充分使用缓存技术,从而在用户使用反射开发模式时,兼顾了性能并保持了组件的通用性和可扩充性:
1、域对象属性和字段集合缓存,即程序只在第一次运行时运用反射获取域对象的属性集合 和字段集合,缓解由于反射造成的性能损失;
2、Sql语句缓存,即对相同业务函数执行的Sql语句只需在第一次运行时构造,缓解了构造Sql语句过程中由于反射和字符串操作可能带来的性能损失;
3、为IPageSplitter接口的Sql Server和Access实现添加Sql语句缓存;
4、数据库参数缓存,数据库参数的使用,能在很大程度上减少由于Sql注入和不同厂商的数据库差异造成的安全问题和Sql语句语法差异问题,但是,每次执行是 构造一对参数同样是很大的开销,所以,本组件中对如Inset、Update这样需要传递的长传参数集合进行缓存,以缓解其构造的开销。
八、域对象生成
这里的与对象指的是,只包含与数据库表字段相对应的映射数据的,数据承载对象。组件提供两种域对象生成的方式:
1、基于反射的域对象生成。提供ObjectFiller类,用于从IdbHelper接口返回的DataTable, IdataReader, 生成相应的与对象实例或集合;
/// ObjectFiller 用来更具指定的对象类型将DataRow、
/// DataTable或IDataReader填充到单个或者数组对象中。
/// </summary>
public class ObjectFiller
{
public ObjectFiller();
public object FillObject(Type objType, DataRow objRow);
public IList FillObjectArray(Type objType, DataTable arrTable);
public IList FillObjectArray(Type objType, IDataReader rdr);
}
2、基于代码生成的与对象生成。在下一章代码生成中的第二种生成方式中,将为生成的基本域对象附加几个静态方法,这些静态方法的作用,就似代替ObjectFiller类,由每个与对象自己,实现域对象的自动构造生成。生成的示例代码如下:
/// 为NorthWind数据库的Region表生成的对应的域对象
/// </summary>
public class Region
{
public int RegionID;
public string RegionDescription;
Static Members
}
九、代码生成
要充分利用本组件的便利性,减少代码编写量,有一个前提,那就是数据库字段名称和对应的域对象属性或者字段名称必须相对应,手工维护无疑是一件痛苦的事,因此,本组件附带一个域对象代码生成器——CodeGenV2,可以直接访问sql server或者access数据库文件生成域对象代码。代码生成器支持将数据库字段映射到与对象的Public Field或Public Property,并且可选是否要生成可以代替上述ObjectFiller类,用来自动构造与对象实例的静态函数,代码生成效果参见上一章的代码实例。
十、组件扩充性
实际的系统开发中可能对性能优化、对默认的语句构造、新数据库的扩充等等都会有新的、本组件本身满足不了的要求,因此在组件设计时充分考虑了其可扩展性,典型的扩展举例如下:
1、继承通用接口,增加兼容于本组件的自定义实现,如支持特定数据库和特殊性能要求的访问;
2、继承默认实现,如在SqlDbHelper类对IDbHelper接口的默认实现上,重写CreateDbConnection()方法,比如用连接池来优化数据库连接性能;
3、重写IPageSplitter的实现以支持自定义的分页实现;
...
十一、基于本组件开发的建议模式
基于本组件的应用开发,建议使用如下的开发模式:
1、业务及数据库建模;
2、构造数据库表和视图(注意: 建议所有程序中尽量不要使用同时访问多张数据库表的Sql语句,而建立足够的视图来换取后期Select语句的自动生成和缓存);
3、运用组件附带的代码生成器生成对应于表结构的域对象(建议对每一视图和原始表都建立域对象,虽然可能造成Insert和Select时同一领域概念有几个不同的域对象,但是,毕竟代码帮你生成了,没有增加工作负担,换来的好处是调用的简化和更大程度上的扩展性);
4、对每一次数据库访问: 首先,用SattementFactory或IPageSplitter接口的实现类构造不分页或可分页的Sql语句,然后,用 IDbParametersFactory接口的实现类构造对应的数据库参数,再调用,IDbHelper的Execute函数执行查询;
5、如果利用反射则调用ObjectFiller或者不利用反射则调用代码生成器生成模式2生成的域对象的静态对象构造函数,将IdnHelper查询返回的DateTable或IDateReader自动填充并返回对应的域对象实例。