Hibernate查询语言:HQL

时间:2022-09-22 09:51:01

前言

  在使用JDBC访问数据库的过程中,一般而言,有两种方式去执行SQL语句,使用java.sql.Statement,及使用java.sql.PreparedStatement,后者继承自前者,拥有前者的一切特征,但增加了预编译的功能,所以在任何时候都推荐使用该接口,尤其是在同一statement会被多次执行,而只是参数不同的情况下,可以得到极大的性能提升。

PreparedStatement的使用方法

  使用PreparedStatement的时候,需要先准备一个包含未知参数的SQL语句,经过预编译后,再使用setInt, setString等方法来设置SQL语句中的参数的,其典型代码如下:

       java.sql.Connection con = null;

       java.sql.PreparedStatement pstmt = null;

      

       String SQL="INSERT INTO CUSTOMER (ID, CHRCOMPANYID) VALUES ( ?, ?)";

       con =……;  //得到连接

       pstmt = con.prepareStatement(SQL); //编译sql语句

      

       pstmt.setInt(1, ID);  //设置参数

       pstmt.setString(2, companyID);

 

       pstmt.executeUpdate();  执行

  } 

这段代码比直接使用Statement的最大的优点就在于可以逐个设置参数,而不用构造一个非常复杂的SQL语句,java.sql.PreparedStatement中还有其它的方法如setDouble, setDate等方法用于设置其它类型的参数。因为参数的转换是由JDBC驱动来做的,这样就无须考虑各个数据库之间的不同,比如日期类型,只需要用setDate方法传递一个java.sql.Date的实例即可,而无须考虑使用什么样格式的字符串。这将使我们的程序更加具有可移植性。

 PreparedStatement的缺点

  如同前面所说,PreparedStatement的最大的优点就是可以动态设置参数,然后其缺点也来源于此,在执行SQL语句出错的时候,我们就没有办法看到完整的SQL语句,甚至也看不到设置的参数,这非常不便于调试。比如说,执行一个非常长的sql语句的时候,报错说插入的值超出范围,大部分JDBC驱动都不会报告是哪个字段出了问题,难道只能执行SQL语句之前将所有参数值手工打印出来吗?而程序调试通过后,这些垃圾代码又都需要清除掉。当一个程序处于不断的调试过程中,这将是极其痛苦的。

 

问题的分析与DebugPreparedStatement的实现

  如何即保持PreparedStatement的优点,同时又便于调试呢?当然,任何时候,想要去掉调试信息的时候,都应该能够方便的去除它。可以想像,这仍然应当“是”一个PreparedStatement,因为我们需要保持原有的功能,只是这个新的类应该多做一些处理,以在出错的时候可以输出更详细的自定义的出错信息,比如参数列表。

是的,看过《设计模式》这本书的朋友应该想到了,这将是一个包装器(Wrapper)模式的应用。这里我不打算介绍该模式的特点及功用,本文的重点不是在此,对此感兴趣的朋友可以查看参考资料中的书目。

我将这个类命名为DebugPreparedStatement,实现了java.sql.PreparedStatement接口。根据包装器模式的结构特点(请参考《设计模式》4.4节),需要有一个指向另一个“真实的”PreparedStatement的指针。同时,由于我们需要记录下SQL语句中所用的参数,以便于出错时输入,所以我们需要一个集合来存放它们。这里我使用了TreeMap,因为它在内部排过序了。

看看类的声明部分吧。

import java.sql.*;

import com.beaconsystem.util.*;

/**

 * 支持参数显示的PreparedStatement实现

 * Creation time: (2001-6-16 0:48:09)

 * @author: SonyMusic

 */

public class DebugPreparedStatement implements PreparedStatement  {

       //指向另一“真实”PreparedStatement

       private PreparedStatement st=null;

       //用于保存参数信息的Map

       private java.util.Map parameterList=new java.util.TreeMap();

}

 

  

 

 

 

 

 

 

 

 

 

 

 

其次,我们需要在设置参数的时候,即setString及类似方法中,能记录下参数的位置和值,这也正是包装器模式的精华部分。请看部分实现:

/**

 * Add debug message.

 * Creation date: (6/14/2002 11:12:30 AM)

 * @param param int

 * @param obj java.lang.Object

 */

protected void addDebug(int param, Object obj) {

       parameterList.put(new Integer(param), obj);

}

public void setInt(int parameterIndex, int x) throws SQLException {

       addDebug(parameterIndex, new Integer(x));

       //在将参数信息添加到Map中后,仍然执行该方法原有功能

       st.setInt(parameterIndex,x);

}

public void setString(int parameterIndex, String x) throws SQLException {

       addDebug(parameterIndex, x);

       st.setString(parameterIndex,x);

}

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

   还有其它各个setXXX方法,这里不再一一列出,但其实现都是类似的。

  最后,在执行SQL语句的时候,如果出错,就在抛出意外的时候,同时将参数信息显示出来,于是我添加了一个“受保护的”方法getDebugMessage,该方法遍历保存参数信息的Map,并将其中的信息整理成String,再返回以供显示。在executeQuery等方法中,将会先调用内部st的相应方法,如果出错,则在原先有出错信息中加入参数信息,并重新构造一个新的java.sql.SQLException,并抛出。请看部分实现:

/**

 * 得到全部的参数信息

 * Creation date: (6/14/2002 11:00:50 AM)

 * @return java.lang.String

 */

protected String getDebugMessage() {

       StringBuffer buf=new StringBuffer(1000);

       buf.append("Parameter Info:");

       java.util.Iterator enu=parameterList.keySet().iterator();

       while (enu.hasNext()) {

              Integer key=(Integer)enu.next();

              Object obj=parameterList.get(key);

              buf.append("");

              buf.append("Parameter ");

              buf.append(key.intValue());

              buf.append(":");

              buf.append(obj);

              if(obj instanceof java.lang.String){

                     buf.append("(Length:");

                     buf.append(((String)obj).length());

                     buf.append(")");

              }

       }

       buf.append("");

       return buf.toString();

}

public ResultSet executeQuery() throws SQLException {

       try {

              ResultSet ret = st.executeQuery();

              return ret;

       }

       catch (SQLException e) {

              throw new SQLException(e.getMessage()+getDebugMessage(), e.getSQLState());

       }

}

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

    其它如executeUpdate方法也与此实现类似,不再重复。

  DebugPreparedStatement类中的其它方法,则是直接调用st的相应,不做任何处理。

DebugPreparedStatement的使用及优点

  类已经写好了,下面将是如何使用的问题了。前面曾经提过,我们希望这个类是易于使用的,并且是易于拆卸的,只在我们需要调试信息的时候才使用,而一旦投入正式运行后,则不希望该类影响到运行速度,也不希望过多的垃圾代码影响程序的美观。

  还是先来看看使用方法吧,请参考前面给出的PreparedStatement的使用

       pstmt = con.prepareStatement(SQL); //编译sql语句

 

       //重新包装了一次,但“看起来”仍然是一个PreparedStatement

       pstmt=new DebugPreparedStatement(pstmt);

 

       //其后的使用与原先无异

       pstmt.setInt(1, ID);  //设置参数

       pstmt.setString(2, companyID);

 

       pstmt.executeUpdate();  执行

  

 

 

 

 

 

 

 

 

 

    在出错的时候,我们也只需要简单的捕捉SQLException意外,并显示其getMessage(),就可以看到详细的参数信息了。

  而在程序运行正确,不再需要调试信息的时候,只需要将新添加的那一行删除,或注释掉,就将恢复成原有的功能。是不是很方便呢?

  该实现最大的优点就在于不影响原先的使用习惯,而且无论是添加或者删除调试信息都是一件非常简单的事情。这种实现方式其实和java.io包中的一些实现是一样的,有兴趣的朋友可以进行一下对比。

总结

  这其实只是一个简单的辅助类,它的应用远没有您在看到本文标题时所想的那么多,其功能也只是简单的扩充,但在特定的时候,却非常的有用。要知道,我写这个类,就是因为在执行一条有差不多40Double型参数的SQL语句时,报错说数值超出范围了,抓头之余写了这个类,帮了我很大的忙。

同时这也是一个优美的模式应用的范例。在和java.io包进行对比的时候,将会发现两者的结构其实是等同的。模式的学习,最重要是要多多的进行比较,思考程序“为什么”以及“如何”写,这样才会有进步。

参考资料

  SunJDBC首页:http://java.sun.com/products/jdbc/index.html ,这里有JDBC相关的介绍,教程,规范等。

  SunJDBC驱动搜索:http://industry.java.sun.com/products/jdbc/drivers ,在这里可以搜索到各种数据库的JDBC驱动,你可能更需要符合JDBC2标准的驱动。

  机械工业出版社出版的《设计模式-可复用面向对象软件的基础》(ISBN:7-111-07575-7):这是一本全面讲述设计模式的好书,作者都是业界公认的设计大师,面向对象编程,此书不可不看。

  com.beaconsystem.util.db.DebugPreparedStatement的源码可以在这里下载

关于作者

  SonyMusic,计算机世界开发者俱乐部Java编程版版主,目前任职于南京丰柏信息技术有限公司,主要开发语言是Java,致力于仓储、物流软件以及条码应用的开发。您可以通过liu@firstbyte..com.cn和他联系。