虽然说我们可以使用Hibernate等OR Mapping工具来实现数据访问,但我一直习惯于写SQL语句来访问数据库。
虽然说我们可以使用Hibernate等OR Mapping工具来实现数据访问,但我一直习惯于写SQL语句来访问数据库。我经常采用的结构是这样的:针对每个数据表实现一个对应的值对象,然后将SQL语句封装在存储过程中,实现一个Persister类调用这些存储过程。客户端就可以使用Persister来操作数据库,值对象作为数据载体。
我一直觉得这样的结构实现起来比较简单,而且执行效率也应该比OR Mapping来的高一些。在大多数的应用中,这样的结构已够用了。但是,这种实现方法最大的问题在于代码量比较大,而且是重复(类似)的代码非常多。
于是,我想到用CodeDOM来实现代码的自动生成应该是可行的。感谢博客园的lichdr的一系列文章,让我很快地实现这一功能。
首先,介绍一下VO的实现,就是从数据库表生成相应的VO类,步骤其实很简单。
我的考虑是,虽然是一个O-R的过程,但我不喜欢映射文件,所以,只能从命名约定来做文章了。我是这样为表命名的,"t_类名",例如"t_Business"表示业务表,字段命名也很规范,如"f_SysID"表示系统ID字段,"f_LoginName"表示登录名字段。
有了这样的严格的命名约定,做映射就非常方便了。三步完成:
1.先查询出库中有多少张以"t_"开头的表,一句话:
select name from sysobjects where substring(name,1,2)='t_'
2.对每张表,查询出它有哪些字段,同样一句话:
select t1.name,t3.name,t1.isnullable from syscolumns t1 join systypes t3 on t1.xusertype=t3.xusertype oin sysobjects t4 on
t1.id=t4.id where t4.name=@tname
![]()
这里得到的分别是"字段名","类型","是否可为空"。
3.然后就是创建类、成员变量和属性了:
1
//cname生成的类名(表名去掉t_字符后加上Info) dbcolumn 字段列表
2
private void generateVOCode(string cname,ArrayList dbcolumn)
3![]()
{
4
CSharpCodeProvider provider = new CSharpCodeProvider();
5
ICodeGenerator generator = provider.CreateGenerator();
6![]()
7
//conf读配置文件得到输出目录
8
StreamWriter writer = new StreamWriter(conf.getOutputDirectory()+@"\info\"+cname+".cs",false);
9
10
CodeCompileUnit unit = new CodeCompileUnit();
11
CodeNamespace nspace = new CodeNamespace(conf.getNamespace()+".info");
12
nspace.Imports.Add(new CodeNamespaceImport("System"));
13
14
CodeTypeDeclaration info = new CodeTypeDeclaration(cname);
15
16
foreach(string[] item in dbcolumn)
17![]()
{
18
//私有成员变量,直接以字段名(包括f_)作变量名
19
CodeMemberField f = new CodeMemberField();
20
f.Name=item[0];
21
f.Type=new CodeTypeReference(mag.getTypeString(item[1]));
22
info.Members.Add(f);
23
//公共属性,以字段名(去掉f_)作属性名
24
CodeMemberProperty p = new CodeMemberProperty();
25
p.Attributes=MemberAttributes.Public;
26
p.Name=item[0].Substring(2);
27
p.Type=f.Type;
28
p.GetStatements.Add(new CodeMethodReturnStatement(
29
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(),f.Name)
30
));
31
p.SetStatements.Add(new CodeAssignStatement(
32
new CodeFieldReferenceExpression(new CodeThisReferenceExpression(),f.Name),
33
new CodePropertySetValueReferenceExpression()
34
));
35
info.Members.Add(p);
36![]()
37
}
38
nspace.Types.Add(info);
39
unit.Namespaces.Add(nspace);
40![]()
41
generator.GenerateCodeFromCompileUnit(unit,writer,new CodeGeneratorOptions());
42
writer.Close();
43
}
4.就OK了。
实现VO的确是非常简单,如果要自动生成Persister的代码就稍复杂一点了。
在Persister类中,我们采用了一个Sington模式(私有构造函数+静态getInstance方法),然后方法与存储过程对应。同样,我们也不用映射文件而是采用约定存储过程命名的方式。例如"p_Business_find"表示业务表的一个查询单条记录过程,这个名称被处理成"p_"表示存储过程,Business加上Persister构成类名"BusinessPersister",find即为方法名。
1.同样,查询一下有多少个存储过程类,就是切出中间的类名,因为"p_Business_find"与"p_Business_list"都会放成BusinessPersister类中,所以只要Business这一段:
select distinct substring(substring(name,3,len(name)-2),1,charindex('_',substring(name,3,len(name)-2))-1) from sysobjects where substring(name,1,2)='p_'
2.对于每个类(例如BusinessPersister),利用CodeDOM生成代码,先把几个基本的方法实现掉:
private void generatePOCode(string cname,ArrayList procs)
![]()
{
CSharpCodeProvider provider = new CSharpCodeProvider();
ICodeGenerator generator = provider.CreateGenerator();
![]()
StreamWriter writer = new StreamWriter(conf.getOutputDirectory()+@"\"+cname+".cs",false);
CodeCompileUnit unit = new CodeCompileUnit();
CodeNamespace nspace = new CodeNamespace(conf.getNamespace());
![]()
//导入多个包
nspace.Imports.Add(new CodeNamespaceImport("System"));
nspace.Imports.Add(new CodeNamespaceImport("System.Collections"));
nspace.Imports.Add(new CodeNamespaceImport("System.Data"));
nspace.Imports.Add(new CodeNamespaceImport("System.Data.SqlClient"));
nspace.Imports.Add(new CodeNamespaceImport("Microsoft.ApplicationBlocks.Data21"));
nspace.Imports.Add(new CodeNamespaceImport(conf.getNamespace()+".info"));
CodeTypeDeclaration po = new CodeTypeDeclaration(cname);
//创建字段conStr,连接字串
CodeMemberField constr = new CodeMemberField("System.String","conStr");
constr.Attributes=MemberAttributes.Private|MemberAttributes.Final;
po.Members.Add(constr);
//静态字段,aInstance,实例自己
CodeMemberField ainst = new CodeMemberField(cname,"aInstance");
ainst.Attributes=MemberAttributes.Private|MemberAttributes.Static;
ainst.InitExpression=new CodePrimitiveExpression(null);
po.Members.Add(ainst);
![]()
//构造函数(私有化)
CodeConstructor ct = new CodeConstructor();
ct.Attributes=MemberAttributes.Private;
ct.Statements.Add(new CodeSnippetStatement("this.conStr=System.Configuration.ConfigurationSettings.AppSettings[\"DBConnStr\"];")); //限定
ct.Statements.Add(new CodeSnippetStatement("if( this.conStr==null || this.conStr.Length==0) throw new Exception();")); //限定
![]()
po.Members.Add(ct);
![]()
//静态方法,getInstance,用来创建实例
CodeMemberMethod getinst = new CodeMemberMethod();
getinst.Name="getInstance";
getinst.ReturnType=new CodeTypeReference(cname);
getinst.Attributes=MemberAttributes.Public|MemberAttributes.Static;
getinst.Statements.Add(
new CodeConditionStatement(new CodeSnippetExpression("aInstance==null")
![]()
,new CodeStatement[]
{new CodeAssignStatement(new CodeVariableReferenceExpression("aInstance"),new CodeObjectCreateExpression(cname,new CodeExpression[]
{}))}
![]()
,new CodeStatement[]
{}
)
);
getinst.Statements.Add(new CodeMethodReturnStatement(new CodeVariableReferenceExpression("aInstance")));
po.Members.Add(getinst);
//私有方法 setParameter(从VO向object[]中填值)
![]()
string votype = procs[0].ToString().Split(new char[]
{'_'},3)[1]+"Info";
![]()
ArrayList cols = mag.getTableColumn("t_"+procs[0].ToString().Split(new char[]
{'_'},3)[1]);
![]()
CodeMemberMethod setp = new CodeMemberMethod();
setp.Name="setParameter";
setp.Attributes=MemberAttributes.Private|MemberAttributes.Final;
setp.ReturnType=new CodeTypeReference("System.Object[]");
setp.Parameters.Add(new CodeParameterDeclarationExpression(votype,"op"));
setp.Statements.Add(new CodeVariableDeclarationStatement("System.Object[]","objs",new CodeSnippetExpression("new object["+cols.Count+"]")));
for(int i=0;i<cols.Count;i++)
![]()
{
string[] col = (string[])cols[i];
setp.Statements.Add(new CodeSnippetStatement("objs["+i+"]=op."+col[0].ToString().Substring(2)+";"));
}
setp.Statements.Add(new CodeMethodReturnStatement(new CodeVariableReferenceExpression("objs")));
po.Members.Add(setp);
//私有方法 getRecord(从reader中读数据到VO中)
CodeMemberMethod getr = new CodeMemberMethod();
getr.Name="getRecord";
getr.Attributes=MemberAttributes.Private|MemberAttributes.Final;
getr.ReturnType=new CodeTypeReference(votype);
getr.Parameters.Add(new CodeParameterDeclarationExpression("SqlDataReader","reader"));
![]()
getr.Statements.Add(new CodeVariableDeclarationStatement(votype,"op",new CodeObjectCreateExpression(votype,new CodeExpression[]
{})));
for(int i=0;i<cols.Count;i++)
![]()
{
string[] col = (string[])cols[i];
getr.Statements.Add(new CodeSnippetStatement("op."+col[0].ToString().Substring(2)+"="+getReadString(col[1],(col[2]=="1"),i)+";"));
}
getr.Statements.Add(new CodeMethodReturnStatement(new CodeVariableReferenceExpression("op")));
![]()
![]()
po.Members.Add(getr);
![]()
//多个公共方法,用于具体工作.(每个存储过程对应一个方法)
foreach(string item in procs)
![]()
{
CodeMemberMethod method = generatePOMethod(item);
po.Members.Add(method);
}
![]()
nspace.Types.Add(po);
unit.Namespaces.Add(nspace);
![]()
generator.GenerateCodeFromCompileUnit(unit,writer,new CodeGeneratorOptions());
writer.Close();
}
3.再查询一下这个类包括几个存储过程,也就是说它有几个方法:
select name from sysobjects where substring(name,1,"+len.ToString()+")='p_"+cname+"_'
4.针对每个方法,生成代码,加到类里面:
private CodeMemberMethod generatePOMethod(string proc)
![]()
![]()
{
CodeMemberMethod method = new CodeMemberMethod();
![]()
method.Name=proc.Split(new char[]
{'_'},3)[2];
method.Attributes=MemberAttributes.Public|MemberAttributes.Final;
//对应的VO类名
![]()
string votype = proc.Split(new char[]
{'_'},3)[1]+"Info";
![]()
ArrayList pars = mag.getProcParameters(proc);
![]()
if( method.Name.StartsWith("list") ) //list开头的方法
![]()
{
method.Statements.Add(
new CodeVariableDeclarationStatement("System.Object[]","objs"
,new CodeSnippetExpression("new object["+pars.Count+"]"))
);
int i=0;
foreach(string[] par in pars)
![]()
{
method.Parameters.Add(
new CodeParameterDeclarationExpression(mag.getTypeString(par[2])
,par[1].Substring(1))
);
method.Statements.Add(
new CodeSnippetStatement("objs["+(i++)+"]="+par[1].Substring(1)+";")
);
}
if( method.Name.IndexOf("PageCount")>0 ) //listPageCount的方法
![]()
{
method.ReturnType=new CodeTypeReference("System.Int32");
![]()
![]()
}
else
![]()
{
method.ReturnType=new CodeTypeReference("ArrayList");
![]()
![]()
}
}
else if( method.Name.StartsWith("find") ) //find开头的方法
![]()
{
method.ReturnType=new CodeTypeReference(votype);
![]()
![]()
}
else
![]()
{
method.ReturnType=new CodeTypeReference("System.Void");
if( method.Name.StartsWith("create") || method.Name.StartsWith("update") ) //create and update 一样的处理
![]()
{
}
else if( method.Name.StartsWith("delete") )
![]()
{
}
else
![]()
{
}
}
return method;
}
在这里,还是依靠名称的严格约定,find表示只查一记录,list表示列出多条记录,create创建新记录,delete删除一条记录,update更新记录...并且,严格约束存储过程的参数,例如create与update的参数依顺序与表字段一样.delete和find则只接收一个SysID主键作为参数.在这样严格的命名约束下,要做的代码与就非常容易了.(我的确是很懒,不想考虑太多的灵活性).
当然,存储过程的参数相对还是比较容易解决,因为可以这样得到参数列表:
select t1.colid,t1.name,t2.name from syscolumns t1 join systypes t2 on t1.xusertype=t2.xusertype join sysobjects t3 on t3.id=t1.id where t3.name='@pname'
但是存储过程的返回值就不是那么好折腾了.一开始我想到用DataSet来处理(因为它可以得到字段的元数据),但是发现不妥,要得到DataSet就需要先执行一下这个SP,但那些需要参数的SP呢?先给个假的数据吧,反正又不是真的要结果,只要结果集的元数据,但是&%##)@...这假数据还真不好给,null?0?呵呵,反正麻烦...而且,象这个存储过程就瞎了,根本没有返回我要的元...
![]()
/**//*
* 根据业务类型列表
*/
CREATE PROCEDURE [dbo].[p_Business_listPageByType]
(
@BizType bigint,@start int,@size int
)
AS
declare @str nvarchar(1000)
set @str= 'SELECT f_SysID,f_LoginID,f_CompanyName,f_CompanyCode,f_BizType,f_BizTyepDesc,f_Status,f_Review,f_OpDate FROM
(SELECT TOP ' + cast( @size as nvarchar(8) ) +' * FROM (SELECT TOP ' + cast( (@start+@size) as nvarchar(8) )
+' * WHERE f_BizType='+ cast(@BizType as nvarchar(8)) +' FROM t_Business ORDER BY f_OpDate DESC) t1 ORDER BY f_OpDate) t2
ORDER BY f_OpDate DESC'
execute sp_executesql @str
![]()
GO
![]()
所以,只能想一个笨办法了,约定存储过程的返回值(呵呵...大家看我的这篇文章,是不是发现出现最多的词就是约定,约束.)...对于find/list等SP返回列的顺序与表一样....这样又解决这个问题了....懒人自有笨办法...
OK...在严格的约束下,这个自动生成访问数据库代码的工具就算完工了.
其实我觉得,就算有这么多的约束,但这些约束本身就是我平时的编码习惯,所以,也不能算是太过苛刻.就算这个工具只给自己用,也能节省不少时间啊...