需求:
(1)提取ResultSet中值到一個類中挺麻煩的,需要new一個變量,使用對應成員的Set方法賦值,能不能根據類的Class,直接提取出ResultSet中的數據,最後返回實例化的類?
(2)可以用PreparedStatement預編譯帶變量的sql語句,在execute之前需要將變量值填充進去,一個一個設置太麻煩了,能不能提供一個類變量,使用類成員變量的值自動填充PreparedStatement?
這樣的功能許多開源的框架可以實現,但是不想因為這麼一點點的需求去學習那麼龐大的一套框架,於是自己實現了一下,總結下自己的實現思路。
實現這套框架下面兩個問題是必須考慮到的:
數據庫表的字段名稱可能和類的成員名稱不一樣。舉個例子數據庫表中經常有這樣的命名modify_time,在類中很少會使用下劃線,一般傾向於modifyTime這樣的命名方式。
預編譯的sql語句中變量的順序和類中成員變量的順序可能會不同。
怎麼將類成員的值對應到預編譯好的PreparedStatement中對應的變量上。
解決問題1和問題2的方法是為類成員提供別名,使用java中的Annotation(標注)機制,標注接口如下:
1 @Target(ElementType.FIELD)
2 @Retention(RetentionPolicy.RUNTIME)
3 public @interface Column {
4 String value();
5 }
標注的例子:
public class Work extends WorkAbstraction{ @Column(value = "coop_type") private Integer coopType; private String subtitle; private String coworker; private String tutor; @Column(value = "create_time") private String createTime; @Column(value = "modify_time") private String modifyTime; private String content; }
在數據庫中有一個work表,有一個字段名是modify_time,對應實體類Work中相應字段被命名為modifyTime,使用@Column(value = "modify_time")為屬性modifyTime提供別名modify_time,對於沒有別名的屬性,默認其和ResultSet中的列名稱相同。
僅僅提供別名是不夠的,從ResultSet中可以獲取到查詢結果的列名稱字段,此時如果想要將ResultSet中的數據填充到某個class中,還需要一個數據結構:
HashMap<Class<?>,HashMap<String,Field>> mappingPools;
HashMap的主鍵是數據類的Class,HashMap的Value還是一個Map,HashMap<String,Field>,該Map的主鍵是類中成員變量的別名,value是java反射類型Field,利用java反射機制就可以很容易通過Field向數據類填充數據。
接下來的問題就是已知class,怎麼解析出上面的HashMap結構,代碼如下:
public class BeanMappingPool { private static Lock lock = new ReentrantLock(); private static HashMap<Class<?>,HashMap<String,Field>> mappingPools; static{ mappingPools = new HashMap<Class<?>,HashMap<String,Field>>(); }; public static HashMap<String,Field> GetFieldsMap(Class<?> objClass){ if(mappingPools.get(objClass) != null) return mappingPools.get(objClass); lock.lock(); if(mappingPools.get(objClass) != null){ lock.unlock(); return mappingPools.get(objClass); } HashMap<String,Field> pools = new HashMap<String,Field>(); for(;objClass != Object.class; objClass = objClass.getSuperclass()) for(Field f: objClass.getDeclaredFields()){ f.setAccessible(true); Column col = f.getAnnotation(Column.class); if(col != null) pools.put(col.value(), f); else pools.put(f.getName(), f); } mappingPools.put(objClass, pools); lock.unlock(); return pools; } }
GetFieldsMap函數從上面的Work中解析出別名和Field的對應關系如下:
{ coop_type= Integer Work.coopType, work_group= String WorkAbstraction.workGroup, content= String Work.content, id= Long WorkAbstraction.id, author= Long WorkAbstraction.author, title= String WorkAbstraction.title, school= String WorkAbstraction.school, tutor= String Work.tutor, name= String WorkAbstraction.name, subtitle= String Work.subtitle, create_time= String Work.createTime, grade= String WorkAbstraction.grade, coworker= String Work.coworker, audit_status= Integer WorkAbstraction.auditStatus, modify_time= String Work.modifyTime, activity_type= Integer WorkAbstraction.activityType }
左邊是類成員的別名,右邊是類成員的Field反射類型。
閱讀GetFieldsMap函數需要注意要解析的類可能有繼承層次,解析時需要迭代向上解析。
將ResultSet中的數據填充到數據類中的例子如下第32行到58行。
public List<Object> query(String preparedSql, Class<?> objCls,Object[] params) { PreparedStatement pstmt = null; ResultSet reSet = null; try { pstmt = conn.prepareStatement(preparedSql); } catch (SQLException e) { e.printStackTrace(); return null; } if(params != null){ try { for (int i = 0; i < params.length; ++i) { if (params[i] != null) pstmt.setObject(i + 1, params[i]); else { Class cls = params[i].getClass(); if (cls.getSimpleName().compareToIgnoreCase("Integer") == 0) pstmt.setNull(i + 1, Types.INTEGER); else if (cls.getSimpleName().compareToIgnoreCase("Long") == 0) pstmt.setNull(i + 1, Types.BIGINT); else if (cls.getSimpleName().compareToIgnoreCase("String") == 0) pstmt.setNull(i + 1, Types.VARCHAR); else pstmt.setNull(i + 1, Types.OTHER); } } } catch (Exception e) { logger.error("[Session.query] " + e.getMessage(),e); } } List<Object> res = new ArrayList<Object>(); try { reSet = pstmt.executeQuery(); List<String> colNames = DBUtil.GetColumnNameSet(reSet); HashMap<String, Field> fieldsMap = BeanMappingPool .GetFieldsMap(objCls); while (reSet.next()) { Object obj = objCls.newInstance(); for (String colName : colNames) { Field field = fieldsMap.get(colName); if (field != null){ Object value = null; Class cls = field.getType(); if (cls.getSimpleName().compareToIgnoreCase("Integer") == 0) value = reSet.getInt(colName); else if (cls.getSimpleName().compareToIgnoreCase("Long") == 0) value = reSet.getLong(colName); else if (cls.getSimpleName().compareToIgnoreCase("String") == 0) value = reSet.getString(colName); else value = reSet.getObject(colName); field.set(obj, value); } } res.add(obj); } } catch (Exception e) { // TODO Auto-generated catch block logger.error("[Session.query] " + e.getMessage(),e); } DBUtil.free(null, pstmt, reSet); return res; }
這裡講清楚了,接下來講講怎麼將類成員的值填充到預編譯好的PreparedStatement中。
問題3是怎麼將類成員的值對應到預編譯好的PreparedStatement中對應的變量上,這裡采用的方法是簡單定義了下sql的方言,例子如下:
UPDATE collective_user SET name=?name, sex=?sex, birthdate=?birthdate, professional_title=?professional_title, subject=?subject, forte=?forte, unit=?unit, city=?city, county=?county, zip_code=?zip_code, contacts=?contacts, address=?address, comment=?comment, modify_time=?modify_time WHERE id=?id;
new PreparedStatement對象時,一般情況下帶變量的sql語句直接類似name=?就可以了,但是為了解決問題3,需要變化成類似name=?name的形式,問號後面是類成員變量的別名,也就是數據庫表中字段的名稱。
使用數據類填充PreparedStatement還需要一個數據結構的幫助:
HashMap<String,Pair<String,List<String>>> sqlMappingPools;
HashMap的key是方言版本的sql,value是一個數據對Pair,Pair的left是解析方言版本的sql得到的規范的sql,right是預編譯好的PreparedStatement中變量名稱列表,此列表中的變量名稱是按照sql中變量的出現順序排好序的。
上面的update語句解析完成後得到的格式Map如下:
< UPDATE collective_user SET name=?,sex=?,birthdate=?,professional_title=?,subject=?, forte=?,unit=?,city=?,county=?,zip_code=?,contacts=?,address=?, comment=?,modify_time=? WHERE id=?; [name, sex, birthdate, professional_title, subject, forte, unit, city, county, zip_code, contacts, address, comment, modify_time, id] >
可以看出上面是規范化的update語句,下面是update語句中對應的變量。
可能會有一個疑問,為什麼問號後面的名稱和等號前面的一樣還需要特別在問號後面標出來?
有些情況下雖然問號出現了,但是並不知道問號對應的數據庫表中的字段的名稱,比如下面的sql語句:
insertinto a_tab values(?,?);
上面的sql方言解決了這個問題。
解析sql方言的代碼如下:
public class PreparedSqlMappingPool { private static HashMap<String,Pair<String,List<String>>> sqlMappingPools; private static Lock lock = new ReentrantLock(); static{ sqlMappingPools = new HashMap<String,Pair<String,List<String>>>(); }; public static Pair<String,List<String>> GetSqlMap(String sql){ if(sqlMappingPools.get(sql) != null) return sqlMappingPools.get(sql); lock.lock(); if(sqlMappingPools.get(sql) != null){ lock.unlock(); return sqlMappingPools.get(sql); } List<String> params = new ArrayList<String>(); StringBuilder strb = new StringBuilder(); boolean append = true; String param = ""; for(char c: sql.toCharArray()){ if(c == '?'){ strb.append("?"); append = false; param = ""; } else if(!append && c == '`') continue; else if(!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_') && !append){ strb.append(c); append = true; params.add(param); param = ""; } else if(append){ strb.append(c); } else param += c; } if(!append && param != "") params.add(param); Pair<String,List<String>> pair = Pair.of(strb.toString(), params); sqlMappingPools.put(sql, pair); lock.unlock(); return pair; } }
將類成員變量填充到PreparedStatement中的代碼如下:
public boolean save(Object obj, String preparedSql) { Pair<String, List<String>> sqlMap = PreparedSqlMappingPool .GetSqlMap(preparedSql); HashMap<String, Field> fields = BeanMappingPool.GetFieldsMap(obj .getClass()); PreparedStatement pstmt = null; boolean res = true; try { pstmt = conn.prepareStatement(sqlMap.left); } catch (SQLException e) { logger.error("[Session.save] " + e.getMessage(),e); return false; } int index = 1; for (String param : sqlMap.right) { Field f = fields.get(param); if (f == null) continue; Object ob = null; try { ob = f.get(obj); } catch (IllegalArgumentException e) { logger.error("[Session.save] " + e.getMessage(),e); } catch (IllegalAccessException e) { e.printStackTrace(); } try { if (ob != null) { pstmt.setObject(index, ob); } else { Class cls = f.getType(); if (cls.getSimpleName().compareToIgnoreCase("Integer") == 0) pstmt.setNull(index, Types.INTEGER); else if (cls.getSimpleName().compareToIgnoreCase("Long") == 0) pstmt.setNull(index, Types.BIGINT); else if (cls.getSimpleName().compareToIgnoreCase("String") == 0) pstmt.setNull(index, Types.VARCHAR); else pstmt.setNull(index, Types.OTHER); } } catch (SQLException e) { logger.error("[Session.save] " + e.getMessage(),e); } ++index; } try { pstmt.execute(); } catch (SQLException e) { res = false; logger.error("[Session.save] " + e.getMessage(),e); } DBUtil.free(null, pstmt, null); return res; }
理解上述代碼需要注意的是java反射類型Field不光可以用於填充類實例,也可以用於從類實例中取出成員數據。