1.概述 我們要將外部系統給的XML文件進行解析,並存入到數據庫。
但是我們並沒有DTD或者Schema,只有一個Word格式的說明文檔;更離譜的是,XML結點樹的結構(即XML結點與XML結點之間的關系)與業務Bean樹的結構(即業務Bean與業務Bean的關系)並不完全一致,比如說,從業務角度講,一只豬有只豬頭,而在XML裡,卻寫成了 pig --content --pighead 的三級關系,無端端多了一個content結點! 沒有DTD/Schema,結構又不規范,我們就沒法用自動化的第三方Java轉換API進行解析,而只能手動地、一個一個地解析。但在手動解析的過程中,我們仍然發現各個結點的解析和入庫中有很多東西是共同的,或者有共同的規律,這些東西可以抽出來作為一個准框架,然後再將結點中不同的部分開放出來,允許具體的結點做具體的實現,並最終形成一個半自動的解析/入庫框架。
為什麼說它是半自動的?它有哪些限制?
自動:不必為每個結點編寫XML 解析代碼和入庫代碼
“半”:需手動地編寫每個JavaBEAN,並手動地為每個BEAN建表
限制:
a.所有業務字段的類型只能設為STRING/VARCHAR,並且非業務字段的類型在BEAN中不能為STRING
b.BEAN名與表名必須相同,或者可以進行一對一映射
c.BEAN的成員變量名必須與XML結點的屬性名/元素名相同,或者可以進行一對一映射
這三種限制都是利用Java反射機制進行自動操作的前提。
2.基本思想
所謂的XML解析,就是將XML結點轉換成JAVABEAN實例,XML結點的ATTRIBUTE值和ELEMENT值就是JAVABEAN實例的成員變量值; 所謂的持久化,就是將JAVABEAN實例變成數據庫對應表中的一條記錄,JavaBEAN實例的成員變量值就是記錄中某個字段的值,或者其他表中某個參考了該記錄的另一條記錄。
而在XML中,JavaBEAN體系中,數據據表關系結構中,結點和結點之間的關系都是樹形的關系。整體的解析和入庫,就是在遍歷樹時執行轉換動作。而我們知道,樹的遍歷是可以用遞歸算法實現的,而遞歸,就不用說了吧,它是實現程序“自動化”的主要途徑之一。
以下是對各“樹”的具體分析:
假設兩個業務實體A和B之間存在聚合關系(父子關系)。那麼具體可分三種情況:
a.B是一個原子字段(即不可再分),並且是A的一個屬性。
XML中,B是A的XML ATTRIBUTE或者A的原子ELEMENT
BEAN中,B是A的成員變量,並且B是一個Java內置的數據類型
數據庫中,B是A表的一個列
b.B是一個復合字段,並且是A的一個屬性,而且和A是1:1關系
XML中,B是A的ELEMENT,並且B有自己的ELEMENT或者ATTRIBUTE
BEAN中,B是A的成員變量,並且程序中有個B類
數據庫中,B表是A表的子表(即B外鍵參考了A表)
c.B是一個復合字段,並且是A的一個屬性,而且和A是N:1關系
XML中,B是A的ELEMENT,並且B有自己的ELEMENT或者ATTRIBUTE
BEAN中,B組成一個類集(List,Set)共同作為A的成員變量,並且程序中有個B類
數據庫中,B表是A表的子表(即B外鍵參考了A表)
了解了這三種情況,接下來就好辦了。程序每抓到一個結點,都要遞歸地進行以下處理:先處理它的原子屬性(情形a),接著處理它的單個子結點(情形b),最後處理它的類集子結點( 情形c)。
3.代碼實現的重點
兩個重點:
a.如何讓業務實體在三棵樹內一一對應好?
b.如何發現樹形關系,比如A的屬性有哪些,A的子結點有哪些?
問題a很簡單,就是讓三棵樹裡相同的業務實體取相同的名字。
a.解析XML時發現 結點X 的 屬性Y 等於 值Z,則執行PropertyUtils.setProperty(結點X , 屬性Y , 值Z)即可。在這裡X,Y,Z是變量,程序不用關心具體的結點和屬性是哪些個。需要注意的是,如果屬性Y是原子字段,則要求屬性Y必須為String類型,否則程序不知道將值Z轉換成哪種類型(注:關於PropertyUtils, 請見apache commons Beanutils )
b.入庫時發現x.getY()=z。如果屬性y是原子字段,則執行SQL insert into X(...,y,...) values (...,z,...),這裡要求y字段必須為varchar/char類型, 以免發生類型轉換錯誤.
關於問題b
XML樹:JDOM, dom4j等都可以直接找到父子關系
BEAN體系:
I.原子屬性。我們限定一個BEAN中所有有業務意義的原子字段的類型都STRING,所有String類型的字段都是業務字段
II.單個子結點。我們讓所有有業務意義的非原子字段都實現一個共同的接口BusiNode,這樣一個BEAN中所有BusiNode成員都是這個BEAN的子結點
III.類集子結點。我們也可以限定所有且只有類集子結點可以使用List或Set類型,這樣可以利用過濾出所有類集子結點。然而,在JAVA1.4及以前的版本裡,程序並不知道過濾出的類集子結點是哪個Class的實例(因為沒有泛型),也就沒辦法實例化一個類集子結點(見後文),因此只能手動注冊類集子結點的屬性名和Class。Java1.5以上的版本我沒用過,不知道可不可以解決這個問題。
數據庫表關系: 這就不用多說了,就是通過外鍵參考。因此每類結點對應的表中,都必須有個外鍵,以參考它的父結點;還必須有個主鍵,以供它的子結點參考。各表的外鍵名必須相同並為一常數,否則程序生成INSERT SQL時才可以不用理會具體表的具體的外鍵名。
程序在解析時,遍歷的是BEAN樹;在持久化時也是。比起XML樹,BEAN樹代表真正的業務結構;比起數據庫表關系樹,BEAN樹才能由父至子地進行先序遍歷
4.其他問題
a.要讓程序知道,原子屬性中哪些是XML結點的屬性,哪些是XML結點的原子ELEMENT。代碼中這是兩個抽象方法,必須讓具體的結點類實現
b.回顧本文概述部分提到的“pig --content --pighead 的三級關系,無端端多了一個content結點”,因此我們要讓程序知道,pighead,pigfoot等結點的子結點,究竟是pig,還是pig下的content。處理不規范XML時要注意這個問題。這也是一個抽象方法,必須讓具體的結點類實現
c.與上一條類似但更變態的,是類集結點的不規范問題。假設一個pig有多個pighead,那結構可能為 pig--pighead,pighead,...,也可能為pig--pigheads--content,content.... 必須讓程序知道某個具體結點用的是哪種模式
5.代碼
核心:多態 + 遞歸
a.接口BusiNode
b.默認的實現
try { PropertyUtils.setProperty(this, attName, elmt.getText()); } catch (Exception e) { throw new RuntimeException(e); } } } } /** * 解析BusiNode屬性 * * @param rootElement */ protected void parseBusiNodeElementFromXml(Element rootElement) { Element xmlElementParent = getXmlElementParent(rootElement); if (xmlElementParent == null) { return; } // 再解析BusiNode屬性 List busiNodePropNames = this.getBusiNodePropNames(); for (int i = 0; i < busiNodePropNames.size(); i++) { try { String attName = (String) busiNodePropNames.get(i); Element elmt = xmlElementParent.element(attName); if (elmt != null) { Field field = this.getClass().getDeclaredField(attName); BusiNode att = (BusiNode) fIEld.getType().newInstance(); att.parseFromXML(elmt); PropertyUtils.setProperty(this, attName, att); } } catch (Exception e) { throw new RuntimeException(e); } } } /** * 解析類集屬性 * * @param rootElement */ protected void parseCollectionPropsFromXml(Element rootElement) { // 先解析XML屬性 Element xmlElementParent = getXmlElementParent(rootElement); if (xmlElementParent == null) { return; } // 最後解析類集屬性 Map collectionPropsMap = this.getCollectionPropsMap(); for (Iterator it = collectionPropsMap.keySet().iterator(); it.hasNext();) { try { String attName = (String) it.next(); Collection coll = (Collection) PropertyUtils.getProperty(this, attName); Class attType = (Class) collectionPropsMap.get(attName); Iterator collElementsIt = this.getCollectionElementIterator( xmlElementParent, attName); // XMLElementParent.elementIterator(attName); while (collElementsIt.hasNext()) { Element collElmt = (Element) collElementsIt.next();
BusiNode sinlgeAtt = (BusiNode) attType.newInstance(); sinlgeAtt.parseFromXML(collElmt); coll.add(sinlgeAtt); } } catch (Exception e) { throw new RuntimeException(e); } } } /** * 從XML中解析出結點。此方法可能拋出RumtimeException */ public void parseFromXML(Element rootElement) { this.parseAttributesFromXml(rootElement); this.parseAtomicElementFromXml(rootElement); this.parseBusiNodeElementFromXml(rootElement); this.parseCollectionPropsFromXML(rootElement); } } /** * 入庫 * JdbcUtil,MyUtils的代碼欠奉 * */ public class BusiNodeDAO { private Long saveBusiNode(BusiNode node, Long parentNodeId) { // 先存儲原子屬性 Long id = saveBareBusiNode(node, parentNodeId); // 再存儲類集屬性 Map collectionPropsMap = node.getCollectionPropsMap(); for (Iterator it = collectionPropsMap.keySet().iterator(); it.hasNext();) { String attName = (String) it.next(); Collection coll = null;
try { coll = (Collection) PropertyUtils.getProperty(node, attName); } catch (Exception e) { throw new RuntimeException("編碼錯誤"); } for (Iterator iitt = coll.iterator(); iitt.hasNext();) { BusiNode subNode = (BusiNode) iitt.next(); saveBusiNode(subNode, id); } } // 最後存儲所有BusiNode屬性 Iterator iitt = node.getBusiNodePropNames().iterator(); while (iitt.hasNext()) { BusiNode subNode = null; try { subNode = (BusiNode) PropertyUtils.getProperty(node, (String) iitt.next()); } catch (Exception e) { throw new RuntimeException("編碼錯誤"); } if (subNode != null) { saveBusiNode(subNode, id); } } return id; } /** * 插入某個BusiNode的根結點,此方法可能拋出RuntimeException * * @param node * @return */ private Long saveBareBusiNode(BusiNode node, Long parentNodeId) {
StringBuffer sbForSql = new StringBuffer(); List paramValues = new ArrayList(); genInsertSqlAndParam(node, parentNodeId, node.getAtomicPropNames(), sbForSql, paramValues); return new Long(JdbcUtil.queryForLong( sbForSql.toString(), paramValues.toArray())); } /** * 生成某個結點的插入語句和paramValues數組,此方法可能拋出RuntimeException * * @param node * @param columnNames * @param sbForSql * @param paramValues */ private void genInsertSqlAndParam(BusiNode node, Long parentNodeId, List columnNames, StringBuffer sbForSql, List paramValues) { sbForSql.append(" insert into "); sbForSql.append(MyUtils.getClassBareName(node.getClass())); List cns = new ArrayList(); cns.addAll(columnNames); cns.add("parentNodeId"); sbForSql.append(MyUtils.encloseWithCurve(MyUtils .joinCollectionStrings(cns, ","))); sbForSql.append(" values "); List qms = new ArrayList(); // 問號 for (Iterator it = columnNames.iterator(); it.hasNext();) { qms.add("?"); String cn = (String) it.next(); try { paramValues.add(PropertyUtils.getProperty(node, cn)); } catch (Exception e) { throw new RuntimeException(e); } } qms.add("?"); // parentNodeId paramValues.add(parentNodeId); sbForSql.append(MyUtils.encloseWithCurve(MyUtil .joinCollectionStrings(qms, ","))); sbForSql.append(";select @@identity"); } }