Java游戏服务器之数据库表存取封装
项目涉及的数据库表并不多,但每个select、insert、update和delete都去手动拼接字符串,是很低效的,尤其在时常要修改结构的情况下。开发的一个目标就是自动化,即能自动实现的事情就不要手动去做;还有一个原则是单一化,即尽量保证数据或逻辑一个入口一个出口。这个需求可以使用一些开源库解决,但因为需求简单,目标明确,没有必要引入多余的第三方库。于是自己写了一个,至少满足当前需求。
数据库表的封装,核心类有两个,表(Table)和记录(Record)。首先需要一个Table类保存数据库表结构的描述,并籍此自动生成相应SQL语句。其次需要一个Record类自动设置SQL参数,并从返回结果集中自动生成逻辑对象。
table类表结构描述可以有两个来源,自动从数据库获取,或从配置表加载。这里选择从配置表加载的方式,一来实现简单,二来应用面更广。
下面是一个账户表的配置示例(user.xml)。
<Tablename="user"primaryKey="user_id"primaryField="userId"> <Columnname="username"field="username"type="2"/> <Columnname="password"field="password"type="2"/> <Columnname="salt"field="salt"type="1"/> <Columnname="reg_time"field="registerTime"type="3"/> <Columnname="last_login_time"field="lastLoginTime"type="3"/> </Table>
只定义了一个主键,有需要可对此扩充。每列name对应数据库表的列名,field对应逻辑对象的成员变量名,type对应字段的类型,比如是int、string、timestamp等,有了名字和类型,就可以使用反射方式自动get和set数据。
Table类读取配置文件获得数据表的结构描述。
publicclassTable<T>{
publicclassTableField{
publicstaticfinalintTYPE_INTEGER=1;
publicstaticfinalintTYPE_STRING=2;
publicstaticfinalintTYPE_TIMESTAMP=3;
publicStringcolumnName="";
publicStringfieldName="";
publicinttype=0;
}
privateStringtableName="";
privateTableFieldprimaryField=newTableField();
privateArrayList<TableField>tableFields=newArrayList<TableField>();
privateStringselectAllSql="";
privateStringselectSql="";
privateStringinsertSql="";
privateStringupdateSql="";
privateStringdeleteSql="";
...
然后生成PrepareStatement方式读写的select、insert、update和delete的预处理SQL字符串。如update:
privateStringgenerateUpdateSql(){
Stringsql="UPDATE"+tableName+"SET";
intsize=tableFields.size();
for(intindex=0;index<size;++index){
TableFieldtableField=tableFields.get(index);
Stringconjunction=index==0?"":",";
StringcolSql=tableField.columnName+"=?";
sql=sql+conjunction+colSql;
}
sql=sql+"WHERE"+primaryField.columnName+"=?";
returnsql;
}
Table类的功能就这么多,下面是关键的Record类,其使用反射自动存取数据。
publicclassRecord<T>{
privateTable<T>table=null;
privateTobject=null;
...
模板参数T即一个表记录对应的逻辑对象。在我们的示例里,即账户数据类:
publicclassUserDataimplementsSerializable{
//用户ID
publicintuserId=0;
//用户名
publicStringusername="";
//密码
publicStringpassword="";
...
有了SQL语句,要先设置参数,才能执行。主键和普通字段分开设置。
publicintsetPrimaryParams(intstart,PreparedStatementpst)throwsException{
Table<T>.TableFieldprimaryField=table.getPrimaryField();
Objectvalue=getFieldValue(primaryField);
value=toDBValue(primaryField,value);
pst.setObject(start,value);
returnstart+1;
}
publicintsetNormalParams(intstart,PreparedStatementpst)throwsException{
ArrayList<Table<T>.TableField>normalFields=table.getNoramlFields();
finalintsize=normalFields.size();
for(intindex=0;index<size;++index){
Table<T>.TableFieldtableField=normalFields.get(index);
Objectvalue=getFieldValue(tableField);
value=toDBValue(tableField,value);
pst.setObject(start+index,value);
}
returnstart+size;
}
就是根据表结构描述,通过反射获取对应字段的值然后设置。
privateObjectgetFieldValue(Table<T>.TableFieldtableField)throwsException{
Fieldfield=object.getClass().getDeclaredField(tableField.fieldName);
returnfield.get(object);
}
toDBValue作用是将Java逻辑类型转成对应数据库类型,比如时间,在逻辑里是Long,而数据库类型是Timestamp。
privateObjecttoDBValue(Table<T>.TableFieldtableField,Objectvalue){
if(tableField.type==TableField.TYPE_TIMESTAMP){
value=newTimestamp((long)value);
}
returnvalue;
}
以设置updateSQL参数为例:
publicvoidsetUpdateParams(PreparedStatementpst)throwsException{
finalintstart=setNormalParams(1,pst);
setPrimaryParams(start,pst);
}
之后执行该SQL语句就可以了。如果是select语句还会返回结果集(ResultSet),从结果集自动生成逻辑对象原理类似,算是一个逆过程,详细参看文末代码。
下面给出一个使用的完整示例:
privatestaticfinalTable<UserData>udTable=newTable<UserData>();
...
udTable.load("user.xml");
...
publicstaticbooleanupdateUserData(UserDatauserData){
booleanresult=false;
Record<UserData>record=udTable.createRecord();
record.setObject(userData);
PreparedStatementpst=null;
try{
Stringsql=udTable.getUpdateSql();
pst=DbUtil.openConnection().prepareStatement(sql);
record.setUpdateParams(pst);
result=pst.executeUpdate()>0;
}catch(Exceptione){
e.printStackTrace();
}finally{
DbUtil.closeConnection(null,pst);
}
returnresult;
}
代码封装得很简易,有更多需求可据此改进。