Query.setDate和Query.setTimestamp区别,附赠log4jdbc的bug

前提

应用中使用hibernate+jtds(1.2.7)+log4jdbc-log4j2-jdbc3-1.16

前景

同事使用hibernate的hql查询当天的业务数据,其中 begin=2017/12/06 00:00:00, end=2017/12/06 23:59:59,具体如下:

  1. //begin 2017/12/06 00:00:00.000
  2. //end 2017/12/06 23:59:59
  3. publicList<FeedbackInfoBean> getFeedbackByFyid(finalInteger fyid,finalDate begin,finalDate end){
  4. Object r = dzjzFoundationDao.getHibernateTemplate().execute(newHibernateCallback(){
  5. @Override
  6. publicObject doInHibernate(Session session)throwsHibernateException,SQLException{
  7. String hql =newString(
  8. "SELECT new com.thunisoft.fy.dzjz.httpapi.prescanner.timer.bean.FeedbackInfoBean ( clfx.CBhDsf as cBhDsf, fd.NAjbs as nAjbs, "+
  9. "fd.NJbfy as nJbfy, fd.NAjlb as nAjlb, jz.CMc as cAh, clfx.dtScanTimesstamp as dScanTimesstamp, clfx.CSerialNum as cSerialNum )"+
  10. " FROM TYwgyDzjzPrescannerClfx clfx, TYwgyDzjzPrescannerInfoFeedback fd, TYwgyDzjzJz jz"+
  11. " WHERE fd.CBhClfx = clfx.CBh AND clfx.CBhAj = jz.CBhAj AND clfx.NState = 1 AND clfx.NJbfy = :JBFY"+
  12. " AND clfx.dtScanTimesstamp > (:start) AND clfx.dtScanTimesstamp < (:end)"
  13. );
  14. Query q = session.createQuery(hql);
  15. try{
  16. q.setParameter("JBFY", fyid);
  17. q.setDate("start", begin);
  18. q.setDate("end", end);
  19. }catch(ParseException e){
  20. thrownewSQLException(String.format("%s or %s is not in a valid date format", begin, end), e);
  21. }
  22. return q.list();
  23. }
  24. });
  25. return(List<FeedbackInfoBean>) r;
  26. }
  27. }

但是,程序就是没有查到数据,jdbc日志输出sql如下:

  1. select tywgydzjzp0_.DT_SCANTIMESSTAMP,tywgydzjzp0_.C_BH_DSF as col_0_0_, tywgydzjzp1_.N_AJBS as col_1_0_, tywgydzjzp1_.N_JBFY as col_2_0_, tywgydzjzp1_.N_AJLB as col_3_0_, tywgydzjzj2_.C_MC
  2. as col_4_0_, tywgydzjzp0_.DT_SCANTIMESSTAMP as col_5_0_, tywgydzjzp0_.C_SERIALNUM as col_6_0_ from YWST.dbo.T_YWGY_DZJZ_PRESCANNER_CLFX tywgydzjzp0_, YWST.dbo.T_YWGY_DZJZ_PRESCANNER_FEEDBCK
  3. tywgydzjzp1_, YWST.dbo.T_YWGY_DZJZ_JZ tywgydzjzj2_ where tywgydzjzp1_.C_BH_CLFX=tywgydzjzp0_.C_BH and tywgydzjzp0_.C_BH_AJ=tywgydzjzj2_.C_BH_AJ and tywgydzjzp0_.N_STATE=1
  4. and tywgydzjzp0_.N_JBFY=2400and tywgydzjzp0_.DT_SCANTIMESSTAMP>'12/06/2017 00:00:00.000'and tywgydzjzp0_.DT_SCANTIMESSTAMP<'12/06/2017 23:59:59.000'

通过sql查询结果是有数据的:

猜想

为此,猜测,内存hibernate或jtds解析参数时,end对应的值为 2017/12/06 00:00:00,即与start对应的值相同,可能出现查询结果为空。

验证

AbstractQueryImpl.setDate 方法具体实现如下:

  1. /**
  2. * AbstractQueryImpl.java
  3. */
  4. publicQuery setDate(String name,Date date){
  5. setParameter(name, date,Hibernate.DATE);
  6. returnthis;
  7. }
  8. /**
  9. * Hibernate.java
  10. * Hibernate <tt>date</tt> type.
  11. */
  12. publicstaticfinalNullableType DATE =newDateType();

DateType中值替换参数时调用的set方法代码如下:

  1. /**
  2. * 至于啥时候调用,就是hibernate bind的时候会用,具体得自己see一眼
  3. * bind
  4. * 注意:此处的sqlDate类型是java.sql.Date
  5. */
  6. publicvoid set(PreparedStatement st,Object value,int index)throwsSQLException{
  7. Date sqlDate;
  8. if( value instanceofDate){
  9. sqlDate =(Date) value;
  10. }
  11. else{
  12. sqlDate =newDate(((java.util.Date) value ).getTime());
  13. }
  14. st.setDate(index, sqlDate);
  15. }

而真正使用的是jtds的JtdsPreparedStatement.setParameter(中间跳过JtdsPreparedStatement.setDate和log4jdbc.PreparedStatementSpy.setDate)有关键代码如下:

  1. if(x instanceofDate){
  2. x =newDateTime((Date) x);
  3. }elseif(x instanceofTime){
  4. x =newDateTime((Time) x);
  5. }elseif(x instanceofTimestamp){
  6. x =newDateTime((Timestamp) x);
  7. }

所以,最后Date或Timestamp对象的还是会转换成jtds的DateTime对象。对于,Date类型,初始化时就没有初始化time部分数据。

  1. DateTime(Date d)throwsSQLException{
  2. dateValue = d;
  3. GregorianCalendar cal =newGregorianCalendar();
  4. cal.setTime(d);
  5. if(cal.get(Calendar.ERA)!=GregorianCalendar.AD)
  6. thrownewSQLException(Messages.get("error.datetime.range.era"),"22007");
  7. year =(short)cal.get(Calendar.YEAR);
  8. month =(short)(cal.get(Calendar.MONTH)+1);
  9. day =(short)cal.get(Calendar.DAY_OF_MONTH);
  10. //请关注到这里,time是没被使用的,时 分 秒都是0
  11. hour =0;
  12. minute =0;
  13. second =0;
  14. millis =0;
  15. packDate();
  16. time = TIME_NOT_USED;
  17. unpacked =true;
  18. }
  19. //but timestamp with 时分秒
  20. DateTime(Timestamp ts)throwsSQLException{
  21. tsValue = ts;
  22. GregorianCalendar cal =newGregorianCalendar();
  23. cal.setTime(ts);
  24. if(cal.get(Calendar.ERA)!=GregorianCalendar.AD)
  25. thrownewSQLException(Messages.get("error.datetime.range.era"),"22007");
  26. if(!Driver.JDBC3){
  27. // Not Running under 1.4 so need to add milliseconds
  28. cal.set(Calendar.MILLISECOND,
  29. ts.getNanos()/1000000);
  30. }
  31. year =(short)cal.get(Calendar.YEAR);
  32. month =(short)(cal.get(Calendar.MONTH)+1);
  33. day =(short)cal.get(Calendar.DAY_OF_MONTH);
  34. hour =(short)cal.get(Calendar.HOUR_OF_DAY);
  35. minute =(short)cal.get(Calendar.MINUTE);
  36. second =(short)cal.get(Calendar.SECOND);
  37. millis =(short)cal.get(Calendar.MILLISECOND);
  38. packDate();
  39. packTime();
  40. unpacked =true;
  41. }

而只有原本是Timestamp类型的才会使得发送到数据库的查询中包含time部分。

hibernate有一个TimeStampType类。(并附上AbstractQueryImpl.setTimestamp代码)

  1. /**
  2. * TimeStampType类
  3. * Hibernate <tt>timestamp</tt> type.
  4. */
  5. publicvoid set(PreparedStatement st,Object value,int index)throwsSQLException{
  6. Timestamp ts;
  7. if(value instanceofTimestamp){
  8. ts =(Timestamp) value;
  9. }
  10. else{
  11. ts =newTimestamp(((java.util.Date) value ).getTime());
  12. }
  13. st.setTimestamp(index, ts);
  14. }
  15. /**
  16. * AbstractQueryImpl类
  17. * Hibernate <tt>timestamp</tt> type.
  18. */
  19. publicQuery setTimestamp(int position,Date date){
  20. setParameter(position, date,Hibernate.TIMESTAMP);
  21. returnthis;
  22. }
  23. /**
  24. * Hibernate类
  25. * Hibernate <tt>timestamp</tt> type.
  26. */
  27. publicstaticfinalNullableType TIMESTAMP =newTimestampType();

so:

如果仅精确到日,注意请使用Query.setDate,就算给的Date有时分秒的值

如果要精确到时分秒的,注意请使用Query.setTimestamp

注意: 版本是jtds-1.2.7,没有验证其他版本是否存在这个差异,不过想想也觉得应该有这个差异,这个差异是正常的:)

有同学会注意到一个问题:为啥jdbc sql日志却输出的是时间格式?请看如下摘取代码:

  1. //PreparedStatementSpy类 仅贴关键代码
  2. protectedvoid argTraceSet(int i,String typeHelper,Object arg){
  3. // 替换的预编译的参数值
  4. synchronized(argTrace)
  5. {
  6. try
  7. {
  8. arg = argTrace.get(argIdx);
  9. }
  10. catch(IndexOutOfBoundsException e)
  11. {
  12. arg ="?";
  13. }
  14. }
  15. if(arg ==null)
  16. {
  17. arg ="?";
  18. }
  19. argIdx++;
  20. dumpSql.append(sql.substring(lastPos,Qpos));// dump segment of sql up to question mark.
  21. lastPos =Qpos+1;
  22. Qpos= sql.indexOf('?', lastPos);
  23. dumpSql.append(arg);
  24. }
  25. //替换的对象
  26. publicvoid setDate(int parameterIndex,Date x)throwsSQLException
  27. {
  28. String methodCall ="setDate("+ parameterIndex +", "+ x +")";
  29. argTraceSet(parameterIndex,"(Date)", x);
  30. try
  31. {
  32. realPreparedStatement.setDate(parameterIndex, x);
  33. }
  34. catch(SQLException s)
  35. {
  36. reportException(methodCall, s);
  37. throw s;
  38. }
  39. reportReturn(methodCall);
  40. }
  41. protectedvoid argTraceSet(int i,String typeHelper,Object arg)
  42. {
  43. String tracedArg;
  44. try
  45. {
  46. //注意这行是关键
  47. tracedArg = rdbmsSpecifics.formatParameterObject(arg);
  48. }
  49. catch(Throwable t)
  50. {
  51. // rdbmsSpecifics should NEVER EVER throw an exception!!
  52. // but just in case it does, we trap it.
  53. log.debug("rdbmsSpecifics threw an exception while trying to format a "+
  54. "parameter object ["+ arg +"] this is very bad!!! ("+
  55. t.getMessage()+")");
  56. // backup - so that at least we won't harm the application using us
  57. tracedArg = arg==null?"null":arg.toString();
  58. }
  59. i--;// make the index 0 based
  60. synchronized(argTrace)
  61. {
  62. // if an object is being inserted out of sequence, fill up missing values with null...
  63. while(i >= argTrace.size())
  64. {
  65. argTrace.add(argTrace.size(),null);
  66. }
  67. if(!showTypeHelp)
  68. {
  69. argTrace.set(i, tracedArg);
  70. }
  71. else
  72. {
  73. argTrace.set(i, typeHelper + tracedArg);
  74. }
  75. }
  76. }
  77. //RdbmsSpecifics类
  78. protectedstaticfinalString dateFormat ="MM/dd/yyyy HH:mm:ss.SSS";
  79. publicString formatParameterObject(Object object)
  80. {
  81. if(object ==null)
  82. {
  83. return"NULL";
  84. }
  85. if(object instanceofString)
  86. {
  87. return"'"+ escapeString((String)object)+"'";
  88. }
  89. elseif(object instanceofDate)
  90. {
  91. return"'"+newSimpleDateFormat(dateFormat).format(object)+"'";
  92. }
  93. elseif(object instanceofBoolean)
  94. {
  95. returnProperties.isDumpBooleanAsTrueFalse()?
  96. ((Boolean)object).booleanValue()?"true":"false"
  97. :((Boolean)object).booleanValue()?"1":"0";
  98. }
  99. else
  100. {
  101. return object.toString();
  102. }
  103. }

结论:因为log4jdbc认为参数对象如果是Date类型的都会按照格式:MM/dd/yyyy HH:mm:ss.SSS 做format,不区分Date和Timestamp,导致拼出的sql带有时分秒精度

如何解决log4jdbc打出的sql在Date处理和真实sql不一致

方案一

找到了log4jdbc-log4j2的github,他们目前最新版本也是1.16。所以,在他们的issues上提了问题,看看他们给不给答复吧! issues link

方案二

逛逛新的log4jdbc开源组件,待续

猜你喜欢

转载自itwork4liu.iteye.com/blog/2404232
今日推荐