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,具体如下:

//begin 2017/12/06 00:00:00.000
//end 2017/12/06 23:59:59
    public List<FeedbackInfoBean> getFeedbackByFyid(final Integer fyid, final Date begin, final Date end) {
        Object r = dzjzFoundationDao.getHibernateTemplate().execute(new HibernateCallback() {
            @Override
            public Object doInHibernate(Session session) throws HibernateException, SQLException {
                String hql = new String(
                    "SELECT new com.thunisoft.fy.dzjz.httpapi.prescanner.timer.bean.FeedbackInfoBean ( clfx.CBhDsf as cBhDsf, fd.NAjbs as nAjbs, " +
                            "fd.NJbfy as nJbfy, fd.NAjlb as nAjlb, jz.CMc as cAh, clfx.dtScanTimesstamp as dScanTimesstamp, clfx.CSerialNum as cSerialNum )" +
                                " FROM TYwgyDzjzPrescannerClfx clfx, TYwgyDzjzPrescannerInfoFeedback fd, TYwgyDzjzJz jz" +
                                    " WHERE fd.CBhClfx = clfx.CBh AND clfx.CBhAj = jz.CBhAj AND clfx.NState = 1 AND clfx.NJbfy = :JBFY"+
                                        " AND clfx.dtScanTimesstamp > (:start) AND clfx.dtScanTimesstamp < (:end)"
                        );
                Query q = session.createQuery(hql);
                try {
                    q.setParameter("JBFY", fyid);
                    q.setDate("start", begin);
                    q.setDate("end", end);
                } catch (ParseException e) {
                    throw new SQLException(String.format("%s or %s is not in a valid date format", begin, end), e);
                }
                return q.list();
            }
        });
        return (List<FeedbackInfoBean>) r;
    }
}

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

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 
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 
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 
and tywgydzjzp0_.N_JBFY=2400 and tywgydzjzp0_.DT_SCANTIMESSTAMP>'12/06/2017 00:00:00.000' and tywgydzjzp0_.DT_SCANTIMESSTAMP<'12/06/2017 23:59:59.000'

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

image.png

猜想

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

验证

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

/**
 * AbstractQueryImpl.java
 */
public Query setDate(String name, Date date) {
        setParameter(name, date, Hibernate.DATE);
        return this;
    }
    
/**
 * Hibernate.java
 * Hibernate <tt>date</tt> type.
 */
public static final NullableType DATE = new DateType();

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

/**
 * 至于啥时候调用,就是hibernate bind的时候会用,具体得自己see一眼 
 *  bind
 * 注意:此处的sqlDate类型是java.sql.Date
 */
public void set(PreparedStatement st, Object value, int index) throws SQLException {
        Date sqlDate;
        if ( value instanceof Date) {
            sqlDate = (Date) value;
        }
        else {
            sqlDate = new Date( ( (java.util.Date) value ).getTime() );
        }
        st.setDate(index, sqlDate);
    }

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

if (x instanceof Date) {
            x = new DateTime((Date) x);
        } else if (x instanceof Time) {
            x = new DateTime((Time) x);
        } else if (x instanceof Timestamp) {
            x = new DateTime((Timestamp) x);
        }

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

DateTime(Date d) throws SQLException {
        dateValue = d;
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(d);

        if (cal.get(Calendar.ERA) != GregorianCalendar.AD)
            throw new SQLException(Messages.get("error.datetime.range.era"), "22007");

        year   = (short)cal.get(Calendar.YEAR);
        month  = (short)(cal.get(Calendar.MONTH) + 1);
        day    = (short)cal.get(Calendar.DAY_OF_MONTH);
        //请关注到这里,time是没被使用的,时 分 秒都是0
        hour   = 0;
        minute = 0;
        second = 0;
        millis = 0;
        packDate();
        time = TIME_NOT_USED;
        unpacked  = true;
    }
    //but timestamp with 时分秒
    DateTime(Timestamp ts) throws SQLException {
        tsValue = ts;
        GregorianCalendar cal = new GregorianCalendar();
        cal.setTime(ts);

        if (cal.get(Calendar.ERA) != GregorianCalendar.AD)
            throw new SQLException(Messages.get("error.datetime.range.era"), "22007");

        if (!Driver.JDBC3) {
            // Not Running under 1.4 so need to add milliseconds
            cal.set(Calendar.MILLISECOND,
                    ts.getNanos() / 1000000);
        }
        year   = (short)cal.get(Calendar.YEAR);
        month  = (short)(cal.get(Calendar.MONTH) + 1);
        day    = (short)cal.get(Calendar.DAY_OF_MONTH);
        hour   = (short)cal.get(Calendar.HOUR_OF_DAY);
        minute = (short)cal.get(Calendar.MINUTE);
        second = (short)cal.get(Calendar.SECOND);
        millis = (short)cal.get(Calendar.MILLISECOND);
        packDate();
        packTime();
        unpacked = true;
    }
    

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

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

    /**
     * TimeStampType类
     * Hibernate <tt>timestamp</tt> type.
     */
    public void set(PreparedStatement st, Object value, int index) throws SQLException {
        Timestamp ts;
        if (value instanceof Timestamp) {
            ts = (Timestamp) value;
        }
        else {
            ts = new Timestamp( ( (java.util.Date) value ).getTime() );
        }
        st.setTimestamp(index, ts);
    }
    
    /**
     * AbstractQueryImpl类
     * Hibernate <tt>timestamp</tt> type.
     */
    public Query setTimestamp(int position, Date date) {
        setParameter(position, date, Hibernate.TIMESTAMP);
        return this;
    }
    
    /**
     * Hibernate类
     * Hibernate <tt>timestamp</tt> type.
     */
    public static final NullableType TIMESTAMP = new TimestampType();

so:

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

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

<font size="5" color="red">注意: 版本是jtds-1.2.7,没有验证其他版本是否存在这个差异,不过想想也觉得应该有这个差异,这个差异是正常的:)</font>

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

//PreparedStatementSpy类 仅贴关键代码
 protected void argTraceSet(int i, String typeHelper, Object arg){
      // 替换的预编译的参数值
      synchronized (argTrace)
      {
        try
        {
          arg = argTrace.get(argIdx);
        }
        catch (IndexOutOfBoundsException e)
        {
          arg = "?";
        }
      }
      if (arg == null)
      {
        arg = "?";
      }

      argIdx++;

      dumpSql.append(sql.substring(lastPos, Qpos));  // dump segment of sql up to question mark.
      lastPos = Qpos + 1;
      Qpos = sql.indexOf('?', lastPos);
      dumpSql.append(arg);
    }
    
    //替换的对象
    public void setDate(int parameterIndex, Date x) throws SQLException
  {
    String methodCall = "setDate(" + parameterIndex + ", " + x + ")";
    argTraceSet(parameterIndex, "(Date)", x);
    try
    {
      realPreparedStatement.setDate(parameterIndex, x);
    }
    catch (SQLException s)
    {
      reportException(methodCall, s);
      throw s;
    }
    reportReturn(methodCall);
  }
  
  protected void argTraceSet(int i, String typeHelper, Object arg)
  {
    String tracedArg;
    try
    {
    //注意这行是关键
      tracedArg = rdbmsSpecifics.formatParameterObject(arg);
    }
    catch (Throwable t)
    {
      // rdbmsSpecifics should NEVER EVER throw an exception!!
      // but just in case it does, we trap it.
      log.debug("rdbmsSpecifics threw an exception while trying to format a " +
        "parameter object [" + arg + "] this is very bad!!! (" +
        t.getMessage() + ")");

      // backup - so that at least we won't harm the application using us
      tracedArg = arg==null?"null":arg.toString();
    }

    i--;  // make the index 0 based
    synchronized (argTrace)
    {
      // if an object is being inserted out of sequence, fill up missing values with null...
      while (i >= argTrace.size())
      {
        argTrace.add(argTrace.size(), null);
      }
      if (!showTypeHelp)
      {
        argTrace.set(i, tracedArg);
      }
      else
      {
        argTrace.set(i, typeHelper + tracedArg);
      }
    }
  }
  
  //RdbmsSpecifics类
  protected static final String dateFormat = "MM/dd/yyyy HH:mm:ss.SSS";
    public String formatParameterObject(Object object)
    {
        if (object == null)
        {
            return "NULL";
        }

        if (object instanceof String)
        {
            return "'" + escapeString((String)object) + "'";
        }
        else if (object instanceof Date)
        {
        
            return "'" + new SimpleDateFormat(dateFormat).format(object) + "'";
        }
        else if (object instanceof Boolean)
        {
            return Properties.isDumpBooleanAsTrueFalse()?
                    ((Boolean)object).booleanValue()?"true":"false"
                        :((Boolean)object).booleanValue()?"1":"0";
        }
        else
        {
            return object.toString();
        }
    }
    

结论:因为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开源组件,待续

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容

  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,869评论 6 13
  • Hibernate: 一个持久化框架 一个ORM框架 加载:根据特定的OID,把一个对象从数据库加载到内存中OID...
    JHMichael阅读 1,958评论 0 27
  • 2017/3/14 RDBMS:关系型数据库管理系统 关系模型独立于语言 SQL有几种不同类型的语言:数据定义语言...
    ancherl阅读 1,586评论 0 6
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,582评论 18 399
  • Dear陌生人: 你很奇怪吧!怎么会有陌生人向你写信?但愿你能花几分钟读完这封信。 这件事不知从何说起?不知道你相...
    Rita95阅读 413评论 0 1