扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
概述
在设计库表时,经常会碰到用于保存"时间值"的字段,如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的<html:text property="expireDate"/>获取数据,另外也方便数据回显到页面中;如果UserActionForm.expireDate采用java.sql.Date类型,则<html:text property="expireDate"/>标签的数据将无法正确地填充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的程序必须对应一个分支。
5、在数据库移植上的比较
由于CHAR类型日期实际上是一个字符类型字段,字符类型是最基本的数据类型,在构造Insert ,Update,Delete,Selete的SQL时,各种数据库对字符类型字段的处理几乎一致,因此在数据库的移植上比较容易。
对于DATE类型的日期,由于不同数据库对日期的操作差异很大,如获取数据库的时间函数,Oracle为sysdate,SqlServer为getdate(),而MySql为now();从Date字段中抽取年的数值,Oracle为extract(year from <date>),SqlServer和MySql均为month(<date>)。由于日期函数在不同数据库差别巨大,带DATE类型日期字段的表在数据库的移植上就不如CHAR类型日期来得简单易行。
也许,有人会说现在都采用Hibernate进行映射ORM了,Hibernate已经屏蔽了具体数据库的不同,何来的数据库移植?这话在一定程度上是没有错的,但是Hibernate框架由于通过对象映射的方法产生SQL语句,有时往往很难获得最优的查询性能的SQL语句。所以,对于一些有性能要求较高的查询,往往采用直接编写SQL语句,或采用iBatis框架,后两者都需要直接使用SQL语句,此时数据库的移植问题就暴露出来了。
不但在数据库的移植问题上,CHAR类型日期比DATE类型日期拥有绝对的优势,在数据的导入/导出,数据传输等方面,CHAR类型日期比DATE类型日期也具有较多的优势。字符型的数据可以直接不失真地用文本或XML表示,而Date类型导出为文本时,如果不指定好转换格式往往难于处理,如2001-01-01的Date数据在转换为文本时,可能变为1st Mon 2001,也可能为2001-01-01 00:00:00 ,甚至可能是01-01-01。这样,在导入时显得难以操作,因为导入/导出都需要指定好日期格式。
6、总结
有一句很经典的关于软件设计的话:如果你的程序逻辑变得很复杂,也许并不是问题域本身的复杂度造成的,往往将归因于设计上的缺陷和瑕疵。同样一个问题,采用不同的策略,往往造成大相径庭的解决复杂度。Spring框架功能强大,我本以为代码一定很复杂,但是当我研读了Spring框架的代码时,才诧异地发现Spring框架的源码很少有超过300行代码的类,类和方法大多简洁明了,真是应了那句大巧若拙,大道至简的话了。
在库表设计时,日期字段究竟是采用CHAR类型日期还是DATE类型日期,在作出选择时,需要考虑在程序逻辑中该数据的加工操作逻辑,毕竟表字段是需要在程序逻辑中使用和操作的,而非仅仅做一个简单的记录而已。
通过上文的分析,我们发现CHAR类型日期可以在较大的程度上简化程序的开发,并且充分利用索引,获取较好的性能。但又一个问题产生了,既然CHAR类型日期这么不好用,各数据库又都提供了Date日期格式,是否Date数据类型就成了尸位素餐的摆设了呢?换言之,Date数据类型适合在什么场合使用呢?笔者个人的建议是,在几乎任何时候都不要使用Date类型作为表字段类型,Date类型仅在存储过程,数据库函数这些数据库程序逻辑代码编写场合使用,即把它看成是一个运算过程的中间工具而不要用其作数据的存储形式。
另外还有一个需要探讨的问题,那就是Date类型长度是7,而Char(8)或Char(14)要比之浪费不少的存储空间,其中这个问题在古代确实是一个大问题,那时候一间茅屋都要合理利用,现在很容易就可以得到广厦千万间。由于硬件性价比持续提升,也就可以使我们采用一些软件上更简便的方法来改善程序的设计,如Java的代码反射,IoC的实现注入,XML形式的数据表示都是受惠于硬件的提升。因此,现在,一般而言,很微小的存储空间占用和性能的影响并不需要设计人员特别的关注,他们要更多关注的往往是如何使逻辑简化,如何使系统更具扩展性,可维护性和移植性上。
查看本文来源
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者