作者:陈雄华
概述
在设计库表时,经常会碰到用于保存"时间值"的字段,如create_date,begin_time,login_time等,举不胜举。针对这些类型的字段,在设置数据类型时,有一个有趣的现象,即其中一些人使用Date类型,而另外一些人使用Char(8)/Char(14)类型。一般而言,初学者,在校学生,甚至老师一般都属于前者,他们一个鲜明的特征是对数据库的理论掌握很好,但普遍缺少实际项目的开发经验;而后者一般是那些有一定项目经验的开发人员。乍一看,这些时间值字段,用Date类型应该是合情合理,天经地义的,为什么有一定项目经验的人偏偏要这样"弃暗投明",这样"特立独行"呢?
这是典型的白猫黑猫问题,理论化的东西很光鲜,但有时在实践中就是不灵光,而一些"旁门左道"的东西却显得更加方便易用。本文将通过一个具体例子的不同开发过程,分析Char类型时间字段为什么在实践中更受欢迎。考虑到篇幅所限,我们仅对Date类型和Char(8)类型的时间值字段作比较分析,对于Date类型和Char(14)类型的分析,相信大家完全可以由此而及彼。
1、比较的例子
我们设计了一个具体的实例,对用Char类型和Date类型的日期进行比较分析,使用的是Oracle数据库,现对该实例进行简单的描述。
假设有一个T_USER表,有一个EXPIRE_DATE(过期日期)字段,要求记录年、月、日的日期数据,对EXPIRE_DATE字段分别采用两种实现方式:
图 1 T_USER表 |
左边的T_USER(1)使用CHAR(8)保存日期值,以yyyymmdd格式保存,如20070606,20070501;而右边的T_USER(2)使用Date数据类型,我们称左边的数据表设计为CHAR类型日期方案,而右边的设计为DATE类型日期方案。
表中的数据当然不会生而有之,我们假设从Web的表单上提交上来,保存到表中,当然还要有查询、统计等操作,我们就通过这些常见的数据操作分析这两个方案的不同,通过这样的分析,孰劣孰优,相信我们就可以进行很好的判断了。
2、从表单添加记录的比较
Struts+Spring+Hibernate是目前Web项目中流行的框架,在这个框架中,Hibernate需要为T_USER生成一个User.java的PO,CHAR类型日期方案的User.expireDate为String类型,而DATE类型日期方案的User.expireDate为java.sql.Date类型,如图 2所示:
图 2 两方案分别对应的User.java PO |
而对应Struts的展现层,需要提供一个UserActionForm,以获取页面表单的提交数据。不管采用哪种日期方案,UserActionForm.expireDate属性类型均为String,因为这样一来,可以直接从Struts的获取数据,另外也方便数据回显到页面中;如果UserActionForm.expireDate采用java.sql.Date类型,则标签的数据将无法正确地填充UserActionForm.expireDate对象属性中。
图 3 UserActionForm.java |
表单提交上来的expireDate是带时间格式的字符串,如2006-06-06,2001-10-12,UserActionForm.expireDate简单地接受该值,在UserAction中,必须用UserActionForm的数据生成持久层所需的PO,即User对象。两种日期方案在数据的转换逻辑的区别分别描述如下:
·CHAR类型日期
由于User.expireDate也是String类型,因此,仅需要将UserActionForm.expireDate属性完全拷贝到User中,然后再将User.expireDate属性的日期格式符"-"去除却可,却将2006-06-06转换为20060606,对应操作逻辑的主要代码如下:
1. User user = new User(); 2. //将userActionForm中的数据拷贝到user对象中 3. BeanUtils.copyProperties(user, userActionForm); 4. //将日期格式符去除,得到数据库存储日期格式,如将2006-06-06转换为20060606 5. user.setExpireDate(user.getExpireDate().replace("-","")); 6. … 7. //调用服务对象,将user保存到T_USER中 8. userService.save(user); |
·DATE类型日期
在DATE类型日期方案中,由于PO User.expireDate属性为java.sql.Date,和UserActionForm.expireDate 存在类型的不匹配,因此需要通过一个转换函数,将String日期转换为java.sql.Date的日期。其主要代码如下:
1. User user = new User(); 2. //由于expireDate不能直接进行拷贝,因此需要逐一拷贝属性 3. BeanUtils.copyPropertie(user, userActionForm,"userId"); 4. BeanUtils.copyPropertie(user, userActionForm,"userName"); 5. //使用转换函数str2Date()将String类型的时间转换为java.sql.Date的时间 6. java.sql.Date expireDate = str2Date(userActionForm.getExpireDate()); 7. //设置expireDate属性 8. user.setExpireDate(expireDate); 9. … 10. //调用服务对象,将user保存到T_USER中 11. userService.save(user); |
通过上面的比较,可以看出,使用DATE时间方案比使用CHAR时间方案在添加数据的处理上要复杂一些,表现在:
1) 由于属性名相同而类型存在不可直接转换的问题将导致无法进行对象间属性批量拷贝,即BeanUtils. copyProperties()批量属性拷贝函数会抛出异常,因此只能手工逐一进行单个具体属性的拷贝,如果属性个数很多,这一机械式的属性拷贝代码块就要相应增大,不但使代码显示臃肿难看,而且直接降低了代码的可维护性,因为一但因表字段名改变,就需要手工调整这段代码。
2) 需要提供一个将String日期串转换为java.sql.Date的转换函数,将年、月、日时间域分别从字串中抽取出来,并转换为int类型,然后利用java.sql.Date(int year,int month,int date)构造函数得到对应的java.sql.Date对象。
3、在数据查询上的比较
假设需要以EXPIRE_DATE字段为条件查询T_USER的记录,由于已经在T_USER.EXPIRE_DATE字段上建立了索引,在查询时需要考虑使用该索引。Web的查询界面如下:
图 4 查询界面 |
日期条件值可以是yyyy、yyyy-mm、yyyy-mm-dd的格式,假如开始日期为2001,结束日期为2002,则表示日期区间为2001-01-01到2002-12-31,如果开始日期为2001,结束日期为2002-02,则表示日期区别为2001-01-01到2002-02-28,以此类推。此外,如果开始日期条件未提供,表示查询所有小于等于结束日期的记录,反之如果结束日期条件未提供,表示查询所有大于等于开始日期的记录。
·CHAR类型日期
CHAR类型日期数据表保存的是CHAR(8)类型的日期,此时可以用简单的方法构造出查询语义丰富,语句结构统一的查询SQL语句,构造方法如下:
1) 去除格式符:将开始,结束查询日期值中的时间格式符去除。
2) 补尾串:将开始日期字符串末尾用0补齐到8位长度,将结束日期字符串末尾用9补齐到8位长度。特别的,如果开始日期为空,则用00000000代替,而结束日期未提供则用99999999代替。
3) 构造查询SQL:用以下SQL语句构造查询语句:
select * from T_USER where EXPIRE_DATE between <开始日期> and <结束日期> |
表 1 CHAR类型日期查询SQL结构
举个例子:假如开始,结束日期值分别为2001、2002-02,则按以上三步的处理过程分别为:
1)去除格式符:2001,200202
2)补尾串:20010000,20020299
3)构造查询SQL:
select * from T_USER where EXPIRE_DATE between '20010000'and '20020299' |
又如开始日期为空,结束日期为200203,则对应的查询SQL为:
select * from T_USER where EXPIRE_DATE between '00000000'and '20020399' |
·DATE类型日期
由于DATE类型日期在数据库表中对应的是Date类型字段,首先,我们不能仿照CHAR类型日期的查询SQL结构构造如下的查询SQL:
select * from T_USER where to_char(EXPIRE_DATE,'yyyymmdd') between <开始日期> and <结束日期>
因为在EXPIRE_DATE上建立了索引,如果在EXPIRE_DATE施加了to_char()的数据库函数,就无法使用该索引,将引发一个全表描述。
所以,还得将开始、结束日期字符串用to_date()数据库函数转换为Date类型,如:
select * from T_USER where EXPIRE_DATE between to_date(<开始日期>,'yyyymmdd') and to_date(<结束日期>,'yyyymmdd') |
表 2 CHAR类型日期查询SQL结构
但由于使用了to_date字符串日期转换函数,就必须保证开始日期和结束日期的字符串必须是语义合法日期字符串,如20010101,20020228,如果是语义错误的日期字符串,如20010000,20020299,to_date函数将发生转换错误,致使上面的查询SQL语句运行错误。因此,只有开始日期和结束日期字符串都合法时,才可以使用上式的查询SQL。
如果开始或结束日期未精确到日,即只精通到年或月,如2001,200202,则在应用程序的服务层,必须对日期串进行语义分析,将其补齐到8位合法日期字符串,如开始日期字符串"2001"就必须补齐为"20010101",而结束日期字符串"200202"就必须先补齐为"20020228"(非润年的平月),而这一转换逻辑处理起来是比较费神费力的,一不小心就可能引入一个Bug。
第二个麻烦的问题是,如果开始日期和结束日期为空,SQL语句又该如何构造呢?如果还按照表 2的SQL结构进行构造,那么就必须回答一个问题:最小开始日期和最大的结束日期分别是多少,因为你不能用"00000000"来代表最小的日期,也不能用"99999999"来代表最大的日期。
为了避免回答这个问题,就需要在开始日期和结束日期为空时分别采用不同结构的查询SQL语句:
select * from T_USER where EXPIRE_DATE >= to_date(<开始日期>,'yyyymmdd') |
表 3 结束日期条件值为空时
select * from T_USER where EXPIRE_DATE <= to_date(<结束日期>,'yyyymmdd') |
表 4 开始日期条件值为空时
综上所述,为了使用在EXPIRE_DATE字段上的索引,DATE类型日期在构造查询SQL上明显比CHAR类型日期复杂,具体表现在以下两点:
1) 需要对日期条件值进行语义分析,以得到精确到日的语义合法的日期字符串。
2) 需要为开始/结束日期条件值均不为空,开始日期条件值为空,结束日期条件值为空三种情况分别构造不同结构的SQL语句,也构造SQL的程序必须对应一个分支
posted on 2006-10-13 13:45
★yesjoy★ 阅读(599)
评论(0) 编辑 收藏 所属分类:
数据库设计