C/C++反射技术的替代方案:解决数据库-实体对应问题
上 一篇 / 下 一篇 2008-10-28 13:53:30
在JAVA中,反射技术 令人赞叹,据此,产 生了大量的ORM和JAVA BEANS。主要解决关系数据库 与对象实体的对应关系,使得数据库的访问简洁方便,与业 务逻辑能够脱离开来。
这些特性使传统的C/C++程序员羡慕不已。许多有识之士开始探讨C/C++环境下的反射技术,以便能够像JAVA那样优雅的写出数据库访问框架。他们的 工作取得了某些成效,但总的来说,使用的工具、技术比较繁杂,未能实现实用的工具框架。
如果能用其他方法解决类似ORM(Object Relational Mapping)的问题,也是一种思路。实际上,我们在数年前就采用了一种方法,当时的目的是在三层客户-服务器模式下,服务器代理执行SQL 语句问题,其中要解决结果集向客户端传送问题,这实际上 就涉及了数据库与实体对象对应的问题。
在C语言里,数据实体就是struct,因此,对应机制我们就称之为SRM(Struct Relational Mapping) 。我们知道,关系数据库有一个关于第一范式的规定,就是每一个元组(ROW),必须由简单属性(columns)构成。因此,我们的结构实体就由简单变量 构成,不包含结构、联合、指针等复杂类型。这种结构我们模仿POJO(Plain Ordinary Java Object),就叫做POCS(Plain Ordinary C Struct)。这个条件非常有利于描述SRM的映射关系。通过一个实例我们来看看这个系统在应用中的表现形态,然后进一步剖析它的内部机制。
数据库表定义
create table seat ( /* 席位表 */
start_date date, /* 始发日期 YYYY-MM-DD*/
beg_station number(4), /* 上车站 */
Train_no varchar2(12), /* 始发车次 */
run_rain varchar2(6), /* 运行·车次 */
on_date date, /* 上车时间 YYYY-MM-DD HH24:mi*/
Carno number(2), /* 车厢号,不分车厢的票车厢号=0 */
seat_type number(4), /*席别,0=无号*/
seat_no number(3), /* 席位号 */
end_station number(3), /* 售前是最远站,售后改为下车站 */
shortest_station number(3), /* 限售以远 */
purpose number(9), /* 用途 */
gride varchar2(50), /* 列车等级,新空调直达特快等 */
flag number(1), /* 标志:0正常,1占用,2已售,3暂不可用,-1禁售 */
used_dev varchar2(16), /* 最后操作终端 */
used_uid varchar2(16), /* 最后操作人 */
used_time date , /* 最后操作时间 YYYY-MM-DD HH24:MI:SS */
primary key(start_date,beg_station,Train_no,Carno,
seat_no,seat_type)
);
SRM 的映射模板,相当于iBates的映像文件:
T_PkgType seat_type[] = { /* 席位表 */
{CH_DATE,YEAR_TO_DAY_LEN,"start_date",YEAR_TO_DAY,-1}, /* 始发日期 YYYY-MM-DD*/
{CH_CHAR,5,"beg_station"}, /* 上车站 */
{CH_CHAR,13,"Train_no"}, /* 始发车次 */
{CH_CHAR,7,"run_train"}, /* 运行车次 */
{CH_DATE,YEAR_TO_MIN_LEN,"on_date",YEAR_TO_MIN,-1}, /* 上车时间 YYYY-MM-DD HH24:mi*/
{CH_TINY,1,"Carno"}, /* 车厢号,不分车厢的票车厢号=0 */
{CH_SHORT,sizeof(short),"seat_type"}, /*席别,0=无号*/
{CH_SHORT,sizeof(short),"seat_no"}, /* 席位号 */
{CH_SHORT,sizeof(short),"end_station"}, /* 售前是最远站,售后改为下车站 */
{CH_SHORT,sizeof(short),"shortest_station"}, /* 限售以远 */
{CH_INT,sizeof(int),"purpose"}, /* 用途 */
{CH_CHAR,51,"gride"}, /* 列车等级,新空调直达特快等 */
{CH_TINY,1,"flag"}, /* 标志:0正常,1占用,2已售,-1禁售 */
{CH_CHAR,17,"used_dev"}, /* 最后操作终端 */
{CH_CHAR,17,"used_uid"}, /* 最后操作人 */
{CH_TIME,sizeof(INT64),"used_time",YEAR_TO_SEC}, /* 最后操作时间 YYYY-MM-DD HH24:MI:SS */
{CH_CHAR,20,"ROWID"},
{-1,0,0}
};
这 里补充一点,T_PkgType的定义如下:
typedef struct {
int type; /*数据类型*/
int len; /*数据长度*/
char *name; /*字段名称*/
char *format; /*格式*/
char offset; /*数据在记录中的位置*/
} T_PkgType;
基本数据类型,在“scpkg.h”中定义:
CH_CHAR /*字符类型,相当于C 语言char[] */
CH_TINY /* 1字节整数*/
CH_SHORT /*短整型,相当于C 语言short*/
CH_INT /*整型,相当于C 语言int */
CH_LONG /*长整型,相当于C 语言long*/
CH_DOUBLE /*浮点型,相当于C 语言double*/
CH_INT4 /* =CH_INT,在任何系统上都是不变的4字节长度 */
CH_INT64 /*8字节整数*/
CH_CLOB /* CLOB,内存中为指针型(char *) */
//CLOB对应ORACLE的long,目前只能读,不能写。结构模板是char*类型,
拆包后,数据还在原来的buffer里,在未处 理完之前,原buffer不要释放。
派生的数据类型,在“sdbc.h”中定义:
CH_DATE /*Oracle 日期型,CH_CHAR派生*/
CH_JUL /*Oracle 日期型,CH_INT派生*/
CH_CJUL /*Oracle字符日期型,CH_INT派生*/
CH_MINUTS /* 分钟型,CH_INT派生*/
CH_TIME /* 秒型,CH_INT64 派生 */
CH_CMINUTS /* 分钟型,CH_INT派生,数据库中是字符型*/
CH_CTIME /* 秒型,CH_INT64 派生 ,数据库中是字符型*/
CH_CNUM /* 字符型派生,数据库中是NUM型*/
NULL 值,在“scpkg.h”中定义:
INTNULL
SHORTNULL
LONGNULL
DOUBLENULL
typedef struct {
char start_date[YEAR_TO_DAY_LEN]; /* 始发日期 YYYY-MM-DD*/
char beg_station[5]; /* 上车站 */
char Train_no[13]; /* 始发车次 */
char run_train[7]; /* 运行车次 */
char on_date[YEAR_TO_MIN_LEN]; /* 开车时间 */
char Carno; /* 车厢号,不分车厢的票车厢号=0 */
short seat_type; /*席别,0=无号*/
short seat_no; /* 席位号 */
short end_station; /* 售前是最远站,售后改为下车站 */
short shortest_station; /* 限售以远 */
int purpose; /* 用途 */
char gride[51]; // 列车等级,新空调直达特快等
char flag; /* 标志:0正常,1占用,2已售,-1禁售 */
char used_dev[17]; /* 最后操作终端 */
char used_uid[17]; /* 最后操作人 */
INT64 used_time; /* 最后操作时间 YYYY-MM-DD HH24:MI:SS */
char ROWID[20];
} seat_s;
程序的前面需要include相应 的.h(工具和POCS.h)和.c(映像表,T_PkgType)
以上模板和数据结构要严格互相对应。当数据库结构改变时,要相应的改变。
下 面是数据访问程序的片段,程序的前部已经根据:上车日期、上车站、运行车次、下车站、席位、用途,取得了上车站(fz),车次(train),下车站 (dz)参数,现在根据这些参数查找席位库:
sprintf(stmt,"select /*+role*/ %s from %s.seat where "
"start_date=to_date('%s','%s') and "
"beg_station = '%s' and "
"Train_no = '%s' and "
"seat_type = %d and "
"end_station >= %d and purpose = %d and "
"flag=0 and rownum<%d order by end_station,Carno,seat_no ",
"for update WAIT 10 SKIP LOCKED",
mkfield(tmp,seat_type,0), //SRM,从模板生成select的数据
SQL_Connect->DBOWN,
strdate,YEAR_TO_DAY, //日期,根据上车日期计算出来的。
t_param->fz.station_code, //上车站
t_param->train.Train_no, //全车次,根据运行车次算出来的
app->xb, //席别
t_param->dz.sequence, //到站顺号,算出来的
app->purpose, //用途
app->quantity ); //数量
/* 生成了这样的语句:
select to_char(start_date,'YYYY-MM-DD') start_date,beg_station,Train_no, run_train,to_char(on_date,'YYYY-MM-DD HH24:MI') on_date,Carno,seat_type, seat_no,end_station,shortest_station,purpose,gride,pro,flag,used_dev,used_uid,to_char(used_time,'YYYY-MM-DD HH24:MI:SS') used_time,ROWID from ticket.seat where start_date=to_date('2008-08-19','YYYY-MM-DD') and beg_station = 'TSHP' and Train_no = 'H6A' and seat_type = 12 and end_station >= 5 and purpose = 0 and flag=0 and rownum<3 order by end_station,Carno,seat_no for update WAIT 10 SKIP LOCKED
*/
int seat_curno=___SQL_Prepare__(SQL_Connect,stmt); //数据访问接口, SQL_Connect是数据库连接句柄,早已打开的。
if(seat_curno<0) { //出错处理
sprintf(msg,"%s,err=%d,%s",stmt,
SQL_Connect->Errno,
SQL_Connect->ErrMsg);
return(seat_curno);
}
seat_s seat;// 定义席位结构
for(i=0;i<app->quantity;i++) { /* 取每条记录 */
JSON_OBJECT *json;
ret=___SQL_Fetch__(SQL_Connect,seat_curno,tmp );// 数据访问接口
if(ret==SQLNOTFOUND || ret==100) break;
if(ret) { //出错处理
if(SQL_Connect->Errno==LOCKED) {
ShowLog(5,"getxw LOCKED continue next");
i--;
continue;
}
break;
}
net_dispack(&seat,tmp,seat_type); // 结果集解析到结构,由于select语句是seat_type生成的,注定其结果集可以解析到seat
seat.flag=1; //进行一点简单处理,占用该席位
seat.used_time=now; //占用时间
strcpy(seat.used_uid,ctx->contex.userid);//占用人
strcpy(seat.used_dev,ctx->contex.devid); //占用窗口
net_pack(tmp,&seat,seat_type) ;// SRM
sprintf(stmt,"update %s.seat %s where ROWID='%s'",
SQL_Connect->DBOWN,
mkupdate(tmp1,tmp,seat_type),seat.ROWID); //SRM
/* 生成如下语句:
update ticket.seat SET (start_date,beg_station,Train_no,run_train,on_date,Carno,seat_type,seat_no,end_station,shortest_station,purpose,gride,pro,flag,used_dev,used_uid,used_time)=(SELECT to_date('2008-08-19','YYYY-MM-DD'),'TSHP','H6A','H6',to_date('2008-08-19 00:26','YYYY-MM-DD HH24:MI'),2,12,1,5,0,0,'G04','',1,'SP0102003','ylh', to_date('2008-08-15 10:24:15','YYYY-MM-DD HH24:MI:SS') FROM DUAL) where ROWID='AAAM3iAAFAAABWQAAA'
注意,ROWID不在其中,mkupdate会自动排除它 */
ret=___SQL_Exec(SQL_Connect,stmt); //发出占用指令
if(ret != 1) { // 修改成功的行数不是1,出错处理
ShowLog(1,"占用席位失败,stmt=%s,err=%d,%s",
stmt,
SQL_Connect->Errno,
SQL_Connect->ErrMsg);
i--;
continue;
}
// 以下用JSON对结果打包
seat.end_station = t_param->dz.sequence;
if(i==0) {
p=strdup("/"seat_common/":");
json = json_object_new_object();
if(!json) {
if(p) free(p);
___SQL_Close__(SQL_Connect,seat_curno);
ShowLog(1,"getxw json_object_new: MEMERR!");
SQL_Connect->Errno=MEMERR;
return MEMERR;
}
struct_to_json(json,&seat,seat_type,"0-3,8-11");// 席位的 公共数据
p=strappend(p,json_object_to_json_string(json));
strcat(p,",/"seat_data/":[");
json_object_put(json);
}
json = json_object_new_object();
if(!json) { // 出错处理
if(p) free(p);
___SQL_Close__(SQL_Connect,seat_curno);
ShowLog(1,"getxw json_object_new: MEMERR!");
SQL_Connect->Errno=MEMERR;
return MEMERR;
}
struct_to_json(json,&seat,seat_type,"Carno,seat_type,seat_no,ROWID"); // 席位的特殊部分。如果我们不怕数据冗余,全部数据打包,就不怕数据变更了:
// struct_to_json(json,&seat,seat_type,0);
p=strappend(p,json_object_to_json_string(json));加入到结果中
strcat(p,",");
json_object_put(json);
} // 循环尾
if(p) strcpy(&p[strlen(p)-1],"]");//完成JSON结构
___SQL_Close__(SQL_Connect,seat_curno); //数据访问接口
[/code]
可以看出,如果席位库发生变化,通常是增加一些字段,只需要修改模板seat_type和POCS:seat_s,这个程序是 不需要变化的。当然,利用这些工具,在C++环境下是很容易写出真正的DAO来的。
本文重点是谈SRM,关于数据库接口部分先放一放,只提及一 下,那些函数(___SQL_*)是包装了OCI的,如果包装了CT_LIB,就是SYBASE了,应用 软 件 换个数据库也是很便捷的。
我们知道,JAVA是用反射来进行ORM的,我们C语 言用什么来进行SRM呢?看了上边的程序,你应该可以猜出来,我们用结构的偏移量算法来“盲人摸象”式的反射结构的成员。
现在来看 “盲人摸象”程序:
[code]
int set_offset(T_PkgType *pkg_type)
{
int i,k;
int ali,dali;
struct {
char a;
long b;
} align;
struct {
char a;
double b;
} dalign;
ali=(int)&align.b - (int)&align-1;
dali=(int)&dalign.b - (int)&dalign-1;
k=0;
for(i=0;pkg_type.type>-1;i++){
pkg_type.offset=k;
if((pkg_type.type&127)!=CH_CLOB)k+=pkg_type.len;
else k+=sizeof(char *);
switch(pkg_type[i+1].type&127) {
case 127:
case CH_CHAR:
case CH_BYTE:
case CH_TINY:
break;
case CH_INT64:
{
struct {
char a;
INT64 b;
} lfali;
int lali;
lali=(int)&lfali.b - (int)&lfali-1;
k=(k+lali)&~lali;
}
break;
case CH_LDOUBLE:
{
struct {
char a;
long double b;
} lfali;
int lali;
lali=(long)&lfali.b - (long)&lfali-1;
k=(k+lali)&~lali;
}
break;
case CH_LDOUBLE:
{
struct {
char a;
long double b;
} lfali;
int lali;
lali=(long)&lfali.b - (long)&lfali-1;
k=(k+lali)&~lali;
}
Break; case CH_SHORT:
k=(k+1)&~1;
break;
case CH_CLOB:
k=(k+(sizeof(char *)-1)) & ~(sizeof(char *)-1);
break;
case CH_FLOAT:
case CH_INT:
k=(k+3)&~3;
break;
case CH_LONG:
default:
k=(k+ali)&~ali;
break;
}
}
pkg_type.offset=k;
return i;
}
[/code]
一 个模板,经过这个处理,每个成员的offset值都被设定成结构的布局。返回值是成员的个数。Pkg_type.Offset就是整个数据区的尺 寸。
下 面的程序从结构(data )中取出一个成员的值,转换成字 符串,放在buf,i是模板中第几个成员,CURDLM是分隔符。这个函数本身不使用分隔符,数据中如果出现分隔符将被转义,CURDLM=0不转义。程 序中出现一些函数用于判断汉字状态和处理转义,与原理无关,略。有些数据类型,日期时间类,是具体系统要用到的,你可以删去。模板pkg_type,使用 前要经过set_offset()处理。
[code]
int get_one(char *buf,void *data,T_PkgType *pkg_type,int i,char CURDLM)
{
int cnt,J,len;
char datebuf[31],*cp1,*cp2;
int type;
char *sp;
short iTiny;
T_PkgType Char_Type[2];
cp1=buf;
*cp1=0;
cnt=0;
cp2=(char *)data;
cp2 += pkg_type.offset;
sp=cp2;
type=pkg_type.type;
if(isnull(cp2,type)) return cnt;
switch(type) {
case CH_CLOB:
Char_Type[0].type=CH_CHAR;
Char_Type[0].len=-1;
Char_Type[0].offset=0;
Char_Type[1].type=-1;
Char_Type[1].len=0;
J=get_one(buf,*(char **)cp2,Char_Type,0,CURDLM);
cnt += J;
break;
case CH_DATE:
case CH_CNUM:
case CH_CHAR:
len=(pkg_type.len>0)?pkg_type.len:strlen(cp2)+1;
for(J=0;J<len-1&&*cp2;J++,cnt++) {
if(!CURDLM) goto norm;
switch(*cp2) {
case ESC_CHAR:
if(cp2>sp && firstcc(sp,cp2-1)) goto norm;
*cp1++=*cp2;
*cp1++=*cp2++;
cnt++;
break;
case '/n':
if(cp2>sp && firstcc(sp,cp2-1)) cp1[-1]&=0x7f;
*cp1++=ESC_CHAR;
*cp1++='n';
cp2++;
cnt++;
break;
default:
if(*cp2==CURDLM) {
if(cp2>sp && firstcc(sp,cp2-1))
goto norm;
*cp1++=ESC_CHAR;
*cp1++='G';
cp2++;
cnt++;
break;
}
norm:
*cp1++=*cp2++;
break;
}
}
*cp1=0;
if(cp2>sp) {
if(firstcc(sp,cp2-1)) cp1[-1] &= 0x7f;
}
break;
case CH_FLOAT:
case CH_DOUBLE:
if(!pkg_type.format)
cnt=sprintf(cp1,"%g", *(double *)cp2);
else
cnt=sprintf(cp1,pkg_type.format,*(double *)cp2);
break;
case CH_LDOUBLE:
if(!pkg_type.format)
cnt=sprintf(cp1,"%Lg", *(long double *)cp2);
else
cnt=sprintf(cp1,pkg_type.format,*(long double *)cp2);
break;
case CH_TINY:
iTiny=*cp2;
cnt=sprintf(cp1,"%hd",iTiny);
break;
case CH_SHORT:
cnt=sprintf(cp1,"%hd",*(short *)cp2);
break;
break;
case CH_INT:
cnt=sprintf(cp1,"%d",*(int *)cp2);
break;
case CH_LONG:
cnt=sprintf(cp1,"%ld",*(long *)cp2);
break;
case CH_INT64:
cnt=sprintf(cp1,FMT64,*(INT64 *)cp2);
break;
case CH_CJUL:
case CH_JUL:
if(pkg_type.format) {
rjultostrfmt(datebuf,*(int *)cp2,
pkg_type.format);
} else {
rjultostrfmt(datebuf,*(int *)cp2,
"YYYYMMDD");
}
cnt=sprintf(cp1,"%s",datebuf);
break;
case CH_MINUTS:
case CH_CMINUTS:
if(pkg_type.format) {
rminstrfmt(datebuf,*(INT4 *)cp2,pkg_type.format);
} else rminstr(datebuf,*(INT4 *)cp2);
cnt=sprintf(cp1,"%s",datebuf);
break;
case CH_TIME:
case CH_CTIME:
if(pkg_type.format) {
rsecstrfmt(datebuf,*(INT64 *)cp2,pkg_type.format);
} else rsecstrfmt(datebuf,*(INT64 *)cp2,"YYYYMMDDHH24MISS");
cnt=sprintf(cp1,"%s",datebuf);
break;
default:
break;
}
return cnt;
}
下 面的程序把字符串cp放到结构(buf)中,i是模板中第几个成员,CURDML是分隔符。
int put_one(void *buf,char *cp,T_PkgType *pkg_type,int i,char CURDLM)
{
int k,ret;
char *cp1;
k=pkg_type.offset;
cp1=(char *)buf;
cp1 += k;
ret=0;
switch(pkg_type.type) {
case CH_CLOB:
*(char **)cp1=cp;
strcpy_esc(cp,cp,-1,CURDLM);
pkg_type.len=strlen(cp);
ret=1;
break;
case CH_DATE:
case CH_CNUM:
case CH_CHAR:
strcpy_esc(cp1,cp,pkg_type.len,CURDLM);
ret=1;
break;
case CH_FLOAT:
*(float *)cp1=0.;
ret=sscanf(cp,"%f",(float *)cp1);
break;
case CH_DOUBLE:
*(double *)cp1=0.;
ret=sscanf(cp,"%lf",(double *)cp1);
break;
case CH_LDOUBLE:
*(long double *)cp1=0.;
ret=sscanf(cp,"%Lf",(long double *)cp1);
break;
case CH_TINY:
{
int tmp;
*cp1=TINYNULL;
ret=sscanf(cp,"%hd",&tmp);
if(ret==1) *cp1=(char)tmp;
break;
}
case CH_SHORT:
*(short *)cp1=SHORTNULL;
ret=sscanf(cp,"%hd",(short *)cp1);
break;
case CH_INT:
*(int *)cp1=INTNULL;
ret=sscanf(cp,"%d",(int *)cp1);
break;
case CH_JUL:
case CH_CJUL:
if(!*cp) *(INT4 *)cp1=TIMENULL;
else if(pkg_type.format){
*(int *)cp1=rstrfmttojul(cp,
pkg_type.format);
} else {
*(int *)cp1= rstrjul(cp);
}
ret=1;
break;
case CH_MINUTS:
case CH_CMINUTS:
if(!*cp) *(INT4 *)cp1=TIMENULL;
else if(pkg_type.format){
*(INT4 *)cp1=rstrminfmt(cp,pkg_type.format);
} else *(INT4 *)cp1=rstrmin(cp);
ret=1;
break;
case CH_TIME:
case CH_CTIME:
if(!*cp) *(INT64 *)cp1=INT64NULL;
else if(pkg_type.format){
*(INT64 *)cp1=rstrsecfmt(cp,pkg_type.format);
} else *(INT64 *)cp1=rstrsecfmt(cp,"YYYYMMDDHH24MISS");
ret=1;
break;
case CH_LONG:
*(long *)cp1=LONGNULL;
ret=sscanf(cp,"%ld",(long *)cp1);
break;
case CH_INT64:
*(INT64 *)cp1=INT64NULL;
ret=sscanf(cp,FMT64,(INT64 *)cp1);
break;
default:
ret=0;
break;
}
return ret;
}
[/code]
到 此,SRM的核心问题-反射问题就解决了,其余是外围工具,利用上述工具组合成例子中的实用程序,如果有兴趣,可以另外开一个专题进行讨论。
下 一个专题,SRM:如何把结构和数据库对应起来。但是这个方法必须使用我们的数据库包装接口,用于接受SRM形成的语句和返回适当格式的结果集,以便 SRM进行处理。这个数据库包装接口还可以独立于数据库,目前已存在ORACLE和SYBASE接口,MYSQL和ODBC接口也不难构造。