长期以来繁琐的JDBC代码使得程序员疲惫不堪,这才导致了ORM思想下各种持久层框架的出现,例如OpenJPA、Hibernate、iBatis、Apache OJB等等。其中主流框架的有三种分别是Apache的OpenJPA、JBoss的Hibernate和Sun的EJB 3.0。究其本质,不外乎都是为了完成3件事:
1、映射数据库表与Java Class,Class的Instance对应数据库的一条条记录
2、通过操作映射Instance自动生成SQL语句并执行
3、管理繁琐的Connection、Statement开闭和ResultSet的读取代码
在明确这3个目标之后,来探讨一下实现的原理,要让框架能记住自己管理的Class类和所有Class的实例到底是对应哪张表的哪个字段,只需要把映射的元数据给缓存下来就可以了。实现方式主要有两种:
1、通过XML映射文件在框架启动时加载cfg.xml文件,例如Hibernate2.0和EJB 2.0
2、在JDK5.0支持元数据级别编程的Annotation之后,可以把配置信息保存在编译后的class文件中并由框架通过反射来读取这些配置信息。
其实无论是哪一种,在结构配置改动的时候都需要重启服务器才能使得改动生效。但相比之下后一种需要重新编译class,而前一种XML是不需要编译器编译的。所以,之前比较提倡第一种,但现在看来,由于JDK5.0->6.0的日益完善,对Annotation的支持使得即使重新编译映射类的Class也不需要以来其他组件,方便之下使得Annotation映射也日益流行起来(元数据技术)。
在公司产品NetApp2.0和DocAve5.0中,有许多表是通过JDBC手工拼接SQL语句来生成的,所有的操作也是传入SQL语句然后依次读取结果集然后再封装成DTO。最初我曾考虑把这些表格也通过OpenJPA来映射,只需要在表格注释中加入Schema区分就可以让这些表分属于不同的Table Space,彼此之间不会有死锁和频繁操作的性能问题。改动如下:
@Table(name=”tb_backup”, schema=”data”) public class Backup{ //……
随着考虑深入,又一次自我否定了这种做法,因为想起了《J2EE Development without EJB》中的一句话:一个架构可达到的简单程度,应该取决于业务需求,而不是技术平台。理想情况下,在项目周期的前期,架构满足业务需求的能力就可以用经验方法评测出来,而不是全凭主观臆断。
在阅读完OpenJPA的官方文档之后,感叹OpenJPA对J2EE5.0关于持久层的规范实现得不但完整而且还提供了许多更复杂更强大的功能。着眼于项目实际考虑,也许在未来的5年甚至10年之内,根本就无缘用到那些强大而配置复杂的功能。在这种思路下,我们为什么要一味去追求框架至上的原则呢?我得到的结论是:最好的框架应该是满足需求前提下最简单轻便的实现。
然后我开始问自己:项目对持久层的需求是什么?回答是这样的:我们不需要太复杂的联表操作和数据管理功能,所使用的数据库只是用来存储一些配置信息,业务方法中也极少数情况下包括同时操作2、3张表的查询和修改。所以,甚至我们都不需要太优化的事务管理,每次只操作一张表,那么大部分业务其实本身就是一个原子操作。
接着考虑到时间的关系,在国庆假期,我开始着手实现了这个小型轻便的JDBC Unit框架。本质上依然会围绕这此文最初讨论的那3个问题,并结合业务需求来设计。
关于框架的功能,我也不觉得应该封装所有的SQL语句,因为我承认我的框架不可能自动拼接完成所有情况的CRUD语句这个事实,实际上连Hibernate也不能。另外我并不打算提供对象查询语句例如HQL这个概念,认为SQL语句足以可读。接下来列举一下核心组件和提供的功能——
注释类TableAnnotation和ColumnAnnotation:提供RUNTIME级别的注释元数据
/** * @author Ant_Yan * Define the mapping table name only */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface TableAnnotation { String table(); } /** * @author Ant_Yan * Define the mapping columns on POJO's fields */ @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ColumnAnnotation { String colName(); String colType(); boolean isUnique() default false; boolean isNullable() default false; boolean isPrimaryKey() default false; }
解析类JdbcParser和ReflectParser:提供映射对象和SQL语句的解析、反射具体类型的解析
public class ReflectParser {
public final static int INT = 1;
public final static int LONG = 2;
public final static int FLOAT = 3;
public final static int DOUBLE =4;
public final static int STRING = 5;
public final static int BINARY = 6;
public final static int CLOB = 7;
public final static int NOT_SUPPORT = 8;
/**
* get instance’s filed value according to the field name
**/
@SuppressWarnings("unchecked")
public static Object getValue(Object instance, String fieldName){}
/**
* get value type according the final static int values
**/
public static int getType(String colType){}
/**
* set field’s value of this instance, according to the file type
**/
@SuppressWarnings("unchecked")
public static void setValue(Object instance, String field, Object value){}
}
/**
* @author Ant_Yan
* Mapping tool use to parse Class info into Table info container
*/
public class JdbcParser {
public static TableDefinition parseClass(Class<?> target){}
public static String parserQuery(String id, String tableName){}
public static String parseTable(TableDefinition tabDef){}
public static String parseInsert(TableDefinition tabDef){}
public static String parserUpdate(TableDefinition tableDef){}
public static String parseDelete(String tableName, String id){}
public static String parserQuery(QueryDefinition query, TableDefinition def){}
}
/**
* @author Ant_Yan
* table information container
*/
public class TableDefinition {
private String tableName;
private Map<String,String> columns = new HashMap<String,String>();
private Map<String,String> colType = new HashMap<String,String>();
private Map<String,String> constraint = new HashMap<String,String>();
}
/**
* @author Ant_Yan
* Define query conditions
*/
public class QueryDefinition {
private String key;
private String constraint;
private Set<String> columns = new HashSet<String>();
private Map<String,String> conditions = new HashMap<String, String>();
}
public class JdbcUnit {
private DataSource dataSource;
private Map<String, TableDefinition> dbMap = new HashMap<String, TableDefinition>();
public void setDataSource(DataSource dataSource){this.dataSource = dataSource;}
public void createTable(Class<?> mappingClass) throws SQLException{}
@SuppressWarnings("unchecked")
public Object get(String id, String key, Class clazz) throws SQLException{}
@SuppressWarnings("unchecked")
public List<Object> getAll(String key, Class clazz) throws SQLException{}
@SuppressWarnings("unchecked")
public List<Object> queryByDefinition(Class clazz, QueryDefinition query) throws Exception{}
public void insert(Object entity, String key) throws SQLException{}
public void update(Object entity, String key) throws SQLException{}
public void delete(String id, String key) throws SQLException{}
public void executeSQL(String sqlScript)throws SQLException{}
public boolean checkSQL(String sqlScript) throws SQLException{}
public List<String> executeList(String sqlScript) throws SQLException{}
public Object executeQuery(String sqlScript, int type)throws SQLException{}
private void fillArgs(Object entity, TableDefinition tableDef, PreparedStatement pst, boolean isUpdate) throws SQLException{}
private void fillValue(ResultSet rs, Object entity, TableDefinition def, Set<String> keys) throws SQLException{}
private boolean isExist(Connection con , String tableName) throws SQLException{}
private TableDefinition checkCached(Class<?> target){}
}
JdbcUnit提供的功能有针对Entity对象的增删改查,若Entity对象字段过多,没有必要每次都映射所有字段,所以针对字符串形式的SQL语句也提供了查询和修改方法,针对项目需求定制了根据各种查询条件封装的QueryDefinition来查询部分字段的操作,结果也只会设置Entity对象的定制查询字段,executeList也是项目中频繁使用的查询某一个字符串字段集合的需求,executeQuery提供仅仅查询特定字段的功能(例如SQL语句中带有count()、max()函数的或者BLOB大字段)。其中两个私有函数fillArgs和fillValue主要封装了从PreparedStatement所有set方法和ResultSet的所有get方法,并利用反射机制绑定这些值到Entity对象上。
JdbcUtil用来封装Connection、Statement的关闭这里就不多说了,需要说明的是针对DataSource提供的Statement,不能调用isClose方法,实际上所有的Connection和Statement都是会被DataSource的实现来缓存管理的,调用close()方法只是还给连接池而已。所以针对查询时我开启了setReadOnly的事务,而在关闭还给连接池之前务必要设定ReadOnly为默认的false,否则在修改操作的时候会抛事务不支持修改的异常。具体代码如下:
public static void closeJdbc(Connection con, Statement st){
try{
if(con!=null&&!con.isClosed()){
con.setAutoCommit(false);
if(con.isReadOnly())
con.setReadOnly(false);
con.close();
}
if(st!=null)
st.close();
}catch(SQLException ex){
ex.printStackTrace();
}
}
大概这个JDBC Unit框架就介绍到这里了,所有的源代码可以在本人的资源里面下载。至于缺陷肯定是存在的,很明显的一点就是关于SQL语句的执行计划缓存问题,我们都知道使用PreparedStatement是可以缓存SQL语句的,然而大量定制的SQL语句我都是用Statement来执行,应该提供一套支持带参数SQL串的执行方法,参数以List形式传入,这样可以缓存这些SQL串提高执行效率。眼下先做到这一步,希望有机会进一步改进。