BG5SBK.FrameworkV2 新版本与帮助文档发布
是开源的时候咯,做的并不完美,但是我知道路是对的,技术自己藏着永远都不会进步,开放出来大家一起讨论一起进步!
在我重新设计和编码持久层的过程中henry无数次的提供了他的宝贵经验和意见,这里要郑重感谢henry
源代码:[2006-09-07-01] BG5SBK.FrameworkV2.rar
Flash版的说明文档:Framework.swf.zip
P.S:代码中其实不单单是持久层的代码,还有一些我平时做项目是积累的常用代码,当然那部分代码不是今天的主角,呵呵
1 前言
2 配置
2.1 数据库连接配置
2.2 CodeSmith的配置
3 基本操作
3.1 查询
3.1.1 简单查询
3.1.2 复合查询
3.1.3 使用SQL函数
3.2 删除与修改
3.3 保存信息
3.3.1 通过Insert对象保存数据
3.3.2 通过实体对象保存数据
3.4 优化执行效率
3.5 其它
1 前言
新的持久层我将其归到FrameworkV2.Data命名空间下,为方便起见本文档中如无特别说明FrameworkV2和V2均表示新设计的持久层。
详细描述新的持久层设计前,我想很有必要回忆和总结下旧的持久层设计。
旧的持久层的设计是基于表映射模型的思路,一个数据库表对应一个实体类(Entity)和一个数据操作类(DataProvider)
这种映射关系如下图所示:
上图中假设有一个表名为“Member”,与它对应的有一个同名的实体类和一个DataProvider。
在业务层代码中对Member的操作并不直接使用MemberDataProvider(更早期的持久层中是直接使用的),而是使用DataProvider的基类DataProvider<T>。
这样设计的目的是在以后的改进中可以去掉DataProvider和实体类型的一一映射关系,转而在DataProvider<T>中使用反射和CodeDom等机制获取实体类和数据库表的映射信息。
这种操作方式类似下图:
图中表示的是业务层执行一个Select操作的过程,可以看出业务层把操作交给DataProvider<Member>,,DataProvider<Member>根据实体对象类型创建具体的DataProvider实例,然后把实际操作交给了MemberDataProvider的实例执行,所以在未来改进中去除掉MemberDataProvider也不会影响到业务层的代码。
上面简要描述了旧持久层的面貌,看似还行,做了几个项目还算不错。
不过从一段时间的使用中旧的持久层的一些弊病开始慢慢的显露出来,以下列出一些比较明显的弊病:
1.基于表映射的DataProvider,随着表的增加DataProvider的文件也再增加,代码难以管理
2.DataProvider只提供对其映射表的操作,无法胜任复杂的查询操作,从而造成在业务层大量暴露SQL脚本
3.DataProvider提供的查询接口,条件的限定传递的是Where语句,排序条件传递的是OrderBy语句,也就是说SQL脚本在这些接口中还是被暴露出来了
4.多个DataProvider间的数据操作不能共享数据库连接和事务,间接的又造成了在一些场合下业务层只能通过编写SQL脚本来执行操作
5.业务操作接口间不能共享数据库连接和事务,这是程序执行效率的上的一个瓶颈
SQL脚本暴露或者说部分暴露有什么不好的地方呢?
1.SQL脚本错误是无法在编译时期被编译器捕获的,因为它只是一个字符串,需要到程序运行起来,并执行到错误的SQL脚本时才会暴露问题
2.SQL脚本的编写并不像程序代码的编写,它不能智能感知的现实,因此开发效率没有程序代码编写效率高
3.SQL脚本经常是直接拼接,降低了系统的安全性和执行效率
4.与数据库类型相关的SQL脚本暴露在业务层,造成程序迁移数据库时的困难
综上所述,SQL脚本暴露是旧持久层的最大弊病,不管是直接导致还是间接导致,总之,数据库操作细节并没有被很好的隐藏。
新的持久层设计充分考虑了原先设计的不足之处,提供了最大灵活性的查询功能,和最简化的操作接口,可以做到几乎不用在业务层中暴露SQL脚本
(就目前使用来看,我还没有直接在业务层写过SQL脚本)
2 配置
FrameworkV2有几个需要配置的地方,要用起来当然要先懂得怎么配置
2.1 数据库连接的配置
数据库连接和以前一样是放在.config文件里,Web项目就放在Web.config,配置方式如下:
2.2 CodeSmith的配置
FrameworkV2中提供了Entity类的CodeSmith代码模板,这里简单讲解下CodeSmith怎么运行模板,其他高级特性大家自己看CodeSmith帮助吧
Step1: 配置CodeSmith的数据库连接
打开CodeSmith,找到项目中Templates目录下的Entity.cst模板,在左边"Schema Explorer"窗口中点击"Manage Data Source"按钮,这时会出现一个
"Data Source Manager"窗口,先选择一个"Northwind"然后Copy它,接着Edit刚刚Copy出来的数据源,将数据库连接字符串改成你自己的就可以了。
Step2: 根据需要修改模板
在Entity.cst的属性栏中(CodeSmith主窗体右手边),有一个EntityNameSpace属性,这是实体类的命名空间,建议将其改为:项目名.Entity
Step3: 生成代码
在模板属性栏的Table属性中选择一个数据库表,然后点击工具栏上的小三角Run,对应表的实体类代码就出现了
Step4: 批量生成代码
通常情况下都是设计完数据库一次性生成实体类代码,一个个的生成有点费劲,所以我做了一个RunEntity.cst模板,用来批量生成实体类代码。
打开RunEntity模板,选择自己的数据库连接,然后设置代码输出路径,点运行,到代码输出路径看看,所有表的实体类代码都在那里了
3 基本操作
3.1 查询
查询操作是FrameworkV2的重头戏,可以说几乎所有的努力都是为了“查询”操作而做。
查询操作需要用到Select类,这是V2中对Select的封装(V2的封装后面会仔细讨论)
Select类的数据操作接口有:
这些操作后面就不再重新说明。
其次查询还需要用到SqlBooleanExpression类,这是V2中对SQL布尔表达式的封装,它令你你可以像写C#条件语句一样写查询的条件
所有类型的字段均支持以下条件限定:
== -- 等于
!= -- 不等于
> -- 大于
< -- 小于
>= -- 大于等于
<= -- 小于等于
Like -- LIKE匹配
NotLike -- NotLike 匹配
In -- 限定在集合中
NotIn -- 限定不在集合中
数值类型和日期类型的字段还另外支持:
Between -- 限定在某个值区间中
NotBetween -- 限定不在某个值区间中
以上的条件限定将会返回一个SQL布尔表达式的对象
SQL布尔表达式支持以下的布尔运算:
& -- 相当于AND
| -- 相当于OR
! -- 相当于~ (取反)
3.1.1 简单查询
//执行Select * From Member 并以实体对象列表返回结果
Select _select = new Select(Member._Any).From(Member._Table);
_select.GetList<Member>();
//执行Select * From Member Where Nickname = 'Peter' Order By JoinTime ASC 并以实体对象列表返回结果
Select _select = new Select(Member._Any).From(Member._Table).Where(Member._Nickname == "Peter").OrderBy(Member._JoinTime.Asc);
_select.GetList<Member>();
3.1.2 复合查询
Select _getTagObjs = new Select(TagBinding._ObjCode).From(TagBinding._Table).Where(TagBinding._TagCode == "Flash");
//执行Select * From MemberFavorites Where Code In(Select ObjCode From TagBinding Where TagCode = 'Flash') 并以实体对象列表返回结果
Select _getFavList = new Select(MemberFavorites._Any).From(MemberFavorites._Table).Where(MemberFavorites._Code.In(_getTagObjs));
_getFavList.GetList<MemberFavorites>();
3.1.3 使用SQL函数
常用SQL函数均在SqlFunctions中,具体用法请参照SQL帮助文档
//执行Select COUNT(*) From Member 并以整数值返回结果
Select _select = new Select(SqlFunctions.COUNT(Member._Any)).From(Member._Table);
_select.GetObject<int>();
3.2 删除与修改
注:Update的设置值可以传递任意个
3.3 保存信息
通常情况下都会通过实体对象的Save方法保存尸体对象信息,Insert方法作为一个可选的方式
3.3.1 通过Insert对象保存数据
3.3.2 通过实体对象保存数据
MemberFile _newFile = new MemberFile();
_newFile.Code = Guid.NewGuid().ToString();
_newFile.ObjCode = objCode;
_newFile.ObjType = objType;
_newFile.DirectoryCode = directory;
_newFile.Save();
3.4 优化执行效率
假设我们需要执行如下的一个复合查询:
Select A.Code From Member As A, MemberInfo As B Where A.Code = B.Code And B.City = '厦门'
我们可以用以下的方式完成任务:
看起来似乎还行,但是我不推荐这样的写法,这样的写法会多次调用At方法来重新生成一个字段,我建议将这些重复操作提前写好:
SqlTypeExpression<string> A_Code = Member._Code.At("A");
SqlTypeExpression<string> B_Code = MemberInfo._Code.At("B");
SqlTypeExpression<string> B_City = MemberInfo._City.At("B");
Select _select = new Select(A_Code).From(Member._Table.As("A"), MemberInfo._Table.As("B"));
_select.Where(A_Code == B_Code & B_City == "厦门");
当做更加复杂的符合查询时这样的写法将会提高代码的可读性和执行效率。
3.5 其它
在新的持久层中加入了一个DbSession类,它可以让不同的数据操作间共享数据库连接和事务,也可以让不同业务操作间共享数据库连接和事务。
DbSession的用法很简单,类似:
这时候两次Select操作就会自动被包含在同一个数据库连接中执行了,业务操作也可以用这样的方式共享连接和事务,这里就不再举例
在我重新设计和编码持久层的过程中henry无数次的提供了他的宝贵经验和意见,这里要郑重感谢henry
源代码:[2006-09-07-01] BG5SBK.FrameworkV2.rar
Flash版的说明文档:Framework.swf.zip
P.S:代码中其实不单单是持久层的代码,还有一些我平时做项目是积累的常用代码,当然那部分代码不是今天的主角,呵呵
1 前言
2 配置
2.1 数据库连接配置
2.2 CodeSmith的配置
3 基本操作
3.1 查询
3.1.1 简单查询
3.1.2 复合查询
3.1.3 使用SQL函数
3.2 删除与修改
3.3 保存信息
3.3.1 通过Insert对象保存数据
3.3.2 通过实体对象保存数据
3.4 优化执行效率
3.5 其它
1 前言
新的持久层我将其归到FrameworkV2.Data命名空间下,为方便起见本文档中如无特别说明FrameworkV2和V2均表示新设计的持久层。
详细描述新的持久层设计前,我想很有必要回忆和总结下旧的持久层设计。
旧的持久层的设计是基于表映射模型的思路,一个数据库表对应一个实体类(Entity)和一个数据操作类(DataProvider)
这种映射关系如下图所示:
上图中假设有一个表名为“Member”,与它对应的有一个同名的实体类和一个DataProvider。
在业务层代码中对Member的操作并不直接使用MemberDataProvider(更早期的持久层中是直接使用的),而是使用DataProvider的基类DataProvider<T>。
这样设计的目的是在以后的改进中可以去掉DataProvider和实体类型的一一映射关系,转而在DataProvider<T>中使用反射和CodeDom等机制获取实体类和数据库表的映射信息。
这种操作方式类似下图:
图中表示的是业务层执行一个Select操作的过程,可以看出业务层把操作交给DataProvider<Member>,,DataProvider<Member>根据实体对象类型创建具体的DataProvider实例,然后把实际操作交给了MemberDataProvider的实例执行,所以在未来改进中去除掉MemberDataProvider也不会影响到业务层的代码。
上面简要描述了旧持久层的面貌,看似还行,做了几个项目还算不错。
不过从一段时间的使用中旧的持久层的一些弊病开始慢慢的显露出来,以下列出一些比较明显的弊病:
1.基于表映射的DataProvider,随着表的增加DataProvider的文件也再增加,代码难以管理
2.DataProvider只提供对其映射表的操作,无法胜任复杂的查询操作,从而造成在业务层大量暴露SQL脚本
3.DataProvider提供的查询接口,条件的限定传递的是Where语句,排序条件传递的是OrderBy语句,也就是说SQL脚本在这些接口中还是被暴露出来了
4.多个DataProvider间的数据操作不能共享数据库连接和事务,间接的又造成了在一些场合下业务层只能通过编写SQL脚本来执行操作
5.业务操作接口间不能共享数据库连接和事务,这是程序执行效率的上的一个瓶颈
SQL脚本暴露或者说部分暴露有什么不好的地方呢?
1.SQL脚本错误是无法在编译时期被编译器捕获的,因为它只是一个字符串,需要到程序运行起来,并执行到错误的SQL脚本时才会暴露问题
2.SQL脚本的编写并不像程序代码的编写,它不能智能感知的现实,因此开发效率没有程序代码编写效率高
3.SQL脚本经常是直接拼接,降低了系统的安全性和执行效率
4.与数据库类型相关的SQL脚本暴露在业务层,造成程序迁移数据库时的困难
综上所述,SQL脚本暴露是旧持久层的最大弊病,不管是直接导致还是间接导致,总之,数据库操作细节并没有被很好的隐藏。
新的持久层设计充分考虑了原先设计的不足之处,提供了最大灵活性的查询功能,和最简化的操作接口,可以做到几乎不用在业务层中暴露SQL脚本
(就目前使用来看,我还没有直接在业务层写过SQL脚本)
2 配置
FrameworkV2有几个需要配置的地方,要用起来当然要先懂得怎么配置
2.1 数据库连接的配置
数据库连接和以前一样是放在.config文件里,Web项目就放在Web.config,配置方式如下:
<appSettings>
<!-- 默认数据库连接 -->
<add key="DefaultDatabase" value="DefaultDB"/>
</appSettings>
<connectionStrings>
<add name="DefaultDB" connectionString="Server=XXXXXX;Database=XXXXXX;uid=sa;pwd=XXXXXX" providerName="System.Data.SqlClient"/>
</connectionStrings>
<!-- 默认数据库连接 -->
<add key="DefaultDatabase" value="DefaultDB"/>
</appSettings>
<connectionStrings>
<add name="DefaultDB" connectionString="Server=XXXXXX;Database=XXXXXX;uid=sa;pwd=XXXXXX" providerName="System.Data.SqlClient"/>
</connectionStrings>
2.2 CodeSmith的配置
FrameworkV2中提供了Entity类的CodeSmith代码模板,这里简单讲解下CodeSmith怎么运行模板,其他高级特性大家自己看CodeSmith帮助吧
Step1: 配置CodeSmith的数据库连接
打开CodeSmith,找到项目中Templates目录下的Entity.cst模板,在左边"Schema Explorer"窗口中点击"Manage Data Source"按钮,这时会出现一个
"Data Source Manager"窗口,先选择一个"Northwind"然后Copy它,接着Edit刚刚Copy出来的数据源,将数据库连接字符串改成你自己的就可以了。
Step2: 根据需要修改模板
在Entity.cst的属性栏中(CodeSmith主窗体右手边),有一个EntityNameSpace属性,这是实体类的命名空间,建议将其改为:项目名.Entity
Step3: 生成代码
在模板属性栏的Table属性中选择一个数据库表,然后点击工具栏上的小三角Run,对应表的实体类代码就出现了
Step4: 批量生成代码
通常情况下都是设计完数据库一次性生成实体类代码,一个个的生成有点费劲,所以我做了一个RunEntity.cst模板,用来批量生成实体类代码。
打开RunEntity模板,选择自己的数据库连接,然后设置代码输出路径,点运行,到代码输出路径看看,所有表的实体类代码都在那里了
3 基本操作
3.1 查询
查询操作是FrameworkV2的重头戏,可以说几乎所有的努力都是为了“查询”操作而做。
查询操作需要用到Select类,这是V2中对Select的封装(V2的封装后面会仔细讨论)
Select类的数据操作接口有:
/// <summary>
/// 执行Select并返回DataSet
/// </summary>
/// <returns>包含结果的DataSet</returns>
public DataSet GetDataSet()
/// <summary>
/// 执行Select并返回DataReader
/// </summary>
/// <returns>包含结果集的DataReader</returns>
public IDataReader GetDataReader()
/// <summary>
/// 执行Select并返回实体对象列表
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
/// <returns>实体对象列表</returns>
public List<T> GetList<T>()
/// <summary>
/// 执行Select并获取单个实体对象
/// </summary>
/// <typeparam name="T">实体对象类型</typeparam>
/// <returns>实体对象</returns>
public T GetEntity<T>()
/// <summary>
/// 执行Select并获取结果中首行首列的值
/// </summary>
/// <returns>结果中首行首列的值</returns>
public object GetObject()
/// <summary>
/// 执行Select并将结果中首行首列的值转为执行类型返回
/// </summary>
/// <typeparam name="T">返回结果类型</typeparam>
/// <returns>结果中首行首列的值</returns>
public T GetObject<T>()
/// 执行Select并返回DataSet
/// </summary>
/// <returns>包含结果的DataSet</returns>
public DataSet GetDataSet()
/// <summary>
/// 执行Select并返回DataReader
/// </summary>
/// <returns>包含结果集的DataReader</returns>
public IDataReader GetDataReader()
/// <summary>
/// 执行Select并返回实体对象列表
/// </summary>
/// <typeparam name="T">实体类型</typeparam>
/// <returns>实体对象列表</returns>
public List<T> GetList<T>()
/// <summary>
/// 执行Select并获取单个实体对象
/// </summary>
/// <typeparam name="T">实体对象类型</typeparam>
/// <returns>实体对象</returns>
public T GetEntity<T>()
/// <summary>
/// 执行Select并获取结果中首行首列的值
/// </summary>
/// <returns>结果中首行首列的值</returns>
public object GetObject()
/// <summary>
/// 执行Select并将结果中首行首列的值转为执行类型返回
/// </summary>
/// <typeparam name="T">返回结果类型</typeparam>
/// <returns>结果中首行首列的值</returns>
public T GetObject<T>()
这些操作后面就不再重新说明。
其次查询还需要用到SqlBooleanExpression类,这是V2中对SQL布尔表达式的封装,它令你你可以像写C#条件语句一样写查询的条件
所有类型的字段均支持以下条件限定:
== -- 等于
!= -- 不等于
> -- 大于
< -- 小于
>= -- 大于等于
<= -- 小于等于
Like -- LIKE匹配
NotLike -- NotLike 匹配
In -- 限定在集合中
NotIn -- 限定不在集合中
数值类型和日期类型的字段还另外支持:
Between -- 限定在某个值区间中
NotBetween -- 限定不在某个值区间中
以上的条件限定将会返回一个SQL布尔表达式的对象
SQL布尔表达式支持以下的布尔运算:
& -- 相当于AND
| -- 相当于OR
! -- 相当于~ (取反)
3.1.1 简单查询
//执行Select * From Member 并以实体对象列表返回结果
Select _select = new Select(Member._Any).From(Member._Table);
_select.GetList<Member>();
//执行Select * From Member Where Nickname = 'Peter' Order By JoinTime ASC 并以实体对象列表返回结果
Select _select = new Select(Member._Any).From(Member._Table).Where(Member._Nickname == "Peter").OrderBy(Member._JoinTime.Asc);
_select.GetList<Member>();
3.1.2 复合查询
Select _getTagObjs = new Select(TagBinding._ObjCode).From(TagBinding._Table).Where(TagBinding._TagCode == "Flash");
//执行Select * From MemberFavorites Where Code In(Select ObjCode From TagBinding Where TagCode = 'Flash') 并以实体对象列表返回结果
Select _getFavList = new Select(MemberFavorites._Any).From(MemberFavorites._Table).Where(MemberFavorites._Code.In(_getTagObjs));
_getFavList.GetList<MemberFavorites>();
3.1.3 使用SQL函数
常用SQL函数均在SqlFunctions中,具体用法请参照SQL帮助文档
//执行Select COUNT(*) From Member 并以整数值返回结果
Select _select = new Select(SqlFunctions.COUNT(Member._Any)).From(Member._Table);
_select.GetObject<int>();
3.2 删除与修改
//执行Delete From MemberFavorites Where Code = '123456'
(MemberFavorites._Code == "123456").Delete();
//执行Update MemberFile Set DirectoryCode = '123456' Where Code = 'XXXXXX'
(MemberFile._Code == "XXXXXX").Update(MemberFile._DirectoryCode.Set("123456"));
(MemberFavorites._Code == "123456").Delete();
//执行Update MemberFile Set DirectoryCode = '123456' Where Code = 'XXXXXX'
(MemberFile._Code == "XXXXXX").Update(MemberFile._DirectoryCode.Set("123456"));
注:Update的设置值可以传递任意个
3.3 保存信息
通常情况下都会通过实体对象的Save方法保存尸体对象信息,Insert方法作为一个可选的方式
3.3.1 通过Insert对象保存数据
Insert _insert = new Insert(
Total._Table,
Total._Code.Set(Guid.NewGuid().ToString()),
Total._ObjCode.Set(objCode),
Total._ObjType.Set(objType),
Total._StrValue.Set(strValue),
Total._TimeValue.Set(timeValue));
_insert.Execute();
Total._Table,
Total._Code.Set(Guid.NewGuid().ToString()),
Total._ObjCode.Set(objCode),
Total._ObjType.Set(objType),
Total._StrValue.Set(strValue),
Total._TimeValue.Set(timeValue));
_insert.Execute();
3.3.2 通过实体对象保存数据
MemberFile _newFile = new MemberFile();
_newFile.Code = Guid.NewGuid().ToString();
_newFile.ObjCode = objCode;
_newFile.ObjType = objType;
_newFile.DirectoryCode = directory;
_newFile.Save();
3.4 优化执行效率
假设我们需要执行如下的一个复合查询:
Select A.Code From Member As A, MemberInfo As B Where A.Code = B.Code And B.City = '厦门'
我们可以用以下的方式完成任务:
Select _select = new Select(Member._Code.At("A")).From(Member._Table.As("A"), MemberInfo._Table.As("B"));
_select.Where(Member._Code.At("A") == MemberInfo._Code.At("B") & MemberInfo._City.At("B") == "厦门");
_select.Where(Member._Code.At("A") == MemberInfo._Code.At("B") & MemberInfo._City.At("B") == "厦门");
看起来似乎还行,但是我不推荐这样的写法,这样的写法会多次调用At方法来重新生成一个字段,我建议将这些重复操作提前写好:
SqlTypeExpression<string> A_Code = Member._Code.At("A");
SqlTypeExpression<string> B_Code = MemberInfo._Code.At("B");
SqlTypeExpression<string> B_City = MemberInfo._City.At("B");
Select _select = new Select(A_Code).From(Member._Table.As("A"), MemberInfo._Table.As("B"));
_select.Where(A_Code == B_Code & B_City == "厦门");
当做更加复杂的符合查询时这样的写法将会提高代码的可读性和执行效率。
3.5 其它
在新的持久层中加入了一个DbSession类,它可以让不同的数据操作间共享数据库连接和事务,也可以让不同业务操作间共享数据库连接和事务。
DbSession的用法很简单,类似:
using (DbSession _session = DbSession.Create())
{
//执行Select * From Member 并以实体对象列表返回结果
Select _select1 = new Select(Member._Any).From(Member._Table);
_select1.GetList<Member>();
//执行Select * From Member Where Nickname = 'Peter' Order By JoinTime ASC 并以实体对象列表返回结果
Select _select2 = new Select(Member._Any).From(Member._Table).Where(Member._Nickname == "Peter").OrderBy(Member._JoinTime.Asc);
_select2.GetList<Member>();
}
{
//执行Select * From Member 并以实体对象列表返回结果
Select _select1 = new Select(Member._Any).From(Member._Table);
_select1.GetList<Member>();
//执行Select * From Member Where Nickname = 'Peter' Order By JoinTime ASC 并以实体对象列表返回结果
Select _select2 = new Select(Member._Any).From(Member._Table).Where(Member._Nickname == "Peter").OrderBy(Member._JoinTime.Asc);
_select2.GetList<Member>();
}
这时候两次Select操作就会自动被包含在同一个数据库连接中执行了,业务操作也可以用这样的方式共享连接和事务,这里就不再举例