最近接手了一個要維護的項目,是用Hibernate2+Oralce8寫成的,因為看到Hibernate3頁出來這麼久了,而且也感覺Hibernate3有它的許多新的特性,如批量刪除和更新,新的HQL語法解析器AST。
升級過程大致按照孫衛琴的那篇文章 如何把Hibernate2.1升級到Hibernate3.0?來做,該替換的替換完,該設置的設置完,程序一跑,當程序執行到向下面這種查詢的時候(Oracle所特有的外連接查詢),報錯。
語句為:(描述為類似語句,把項目中的實際表名隱去了)
session.createQuery("select t1.c1,t2.c1 from Table1 t1,Table2 t2 where t1.c1=t2.c1(+)").list();
出錯信息為:
org.hibernate.hql.ast.QuerySyntaxException: unexpected token: ) near line 1, column 106 [select t1.c1,t2.c1 from Table1 t1,Table2 t2 where t1.c1=t2.c1(+)]
at org.hibernate.hql.ast.QuerySyntaxException.convert(QuerySyntaxException.java:31)
at org.hibernate.hql.ast.QuerySyntaxException.convert(QuerySyntaxException.java:24)
at org.hibernate.hql.ast.ErrorCounter.throwQueryException(ErrorCounter.java:59)
at org.hibernate.hql.ast.QueryTranslatorImpl.parse(QueryTranslatorImpl.java:258)
at org.hibernate.hql.ast.QueryTranslatorImpl.doCompile(QueryTranslatorImpl.java:157)
at org.hibernate.hql.ast.QueryTranslatorImpl.compile(QueryTranslatorImpl.java:111)
at org.hibernate.engine.query.HQLQueryPlan.<init>(HQLQueryPlan.java:77)
at org.hibernate.engine.query.HQLQueryPlan.<init>(HQLQueryPlan.java:56)
at org.hibernate.engine.query.QueryPlanCache.getHQLQueryPlan(QueryPlanCache.java:72)
at org.hibernate.impl.AbstractSessionImpl.getHQLQueryPlan(AbstractSessionImpl.java:133)
at org.hibernate.impl.AbstractSessionImpl.createQuery(AbstractSessionImpl.java:112)
at org.hibernate.impl.SessionImpl.createQuery(SessionImpl.java:1623)
再回頭看看孫衛琴的那篇升級注意事項中 1.3 查詢語句的變化 提到Hibernate3.0 采用新的基於ANTLR的HQL/SQL查詢翻譯器ASTQueryTranslator,它已經不支持像Oracle8i和Sybase11那樣的 THETA-STYLE 連接查詢方言。
解決這一問題的辦法有兩種:
(1)改為使用支持ANSI-STYLE連接查詢的方言,像 LEFT OUTER JOIN .. ON ..的寫法
(2)也可改用 Hibernate2的查詢翻譯器,可在 hibernate.cfg.xml 中進行配置。
因第一種方法,需要在映射文件中配置PO 間的X 對X的關聯關系才能用,如過哪位朋友在不配置 PO 間關聯關系時也能用LEFT OUTER JOIN .. ON ..的寫法連接查詢,能告訴我怎麼做的號嗎?讓咱也學一招,先謝了!
所以想想還是在 hibernate.cfg.xml 中配置
<property name= "query.factory_class">
org.hibernate.hql.classic.ClassicQueryTranslatorFactory
</property>
<property name= "query.factory_class">
org.hibernate.hql.classic.ClassicQueryTranslatorFactory
</property>
注:hibernate3默認的HQL語法翻譯器的配置為:
<property name= "query.factory_class">
org.hibernate.hql.classic.ASTQueryTranslatorFactory
</property>
<property name= "query.factory_class">
org.hibernate.hql.classic.ASTQueryTranslatorFactory
</property>
使用傳統的hibernat2所用的HQL語法翻譯器。然後程序再跑一跑,剛剛那個(+)的地方是沒有錯了,可是新麻煩有冒起來了,程序執行到
session.createQuery("delete User u where u.name='Unmi'").executeUpdate();
有報錯了:
org.hibernate.QueryException: query must begin with SELECT or FROM: delete [delete com.unmi.User where u.name='Unmi']
原來舊的HQL語法解析器不支持 delete User 的寫法,hibernate2在刪除持久化對象時必須寫成
session.delete("delete User u where u.name='Unmi'");
然而新的 org.hibernate.Session 的接口方法已去除了 Session.delete(String hql)方法,看來這條路也是受阻了。正是兩頭受難,無奈之時暫時放棄了升級的念頭,把該還原的地方都恢復舊模樣了。
過了好一段時間,也就是個把月吧……
心裡總也覺不甘心,覺得事情總有解決的辦法,於是采用了終極辦法:從原代碼下手,進行單步的跟蹤,看看hibernate3何時進行HQL到SQL的轉換,何時取用配置的語法翻譯器。
下面要解決的一個課題就是:
如何讓Hibernate3既能使用新的Delete和Update語法,又能使用 Oracle Theta-Style 的 t1.c1=t2.c1(+)外連接寫法
其中的語法翻譯器如何把傳入的一條HQL語句拆解進行分析這裡就不詳敘,不過最好還是要明白一點:
Classic語法翻譯器會把傳入的t1.c1=t2.c1(+)中的(+)作為一個整體,不拆開來,而AST語法分析器卻會把其中的(+)依括號拆成 ”(” , ”+” , ”)”三部分。
我們首先來看看HQL語法翻譯工廠接口 QueryTranslatorFatory 有兩個接口方法:
public QueryTranslator createQueryTranslator(String queryIdentifier, String queryString,
Map filters, SessionFactoryImplementor factory);
public QueryTranslator createFilterTranslator(String queryIdentifier, String queryString,
Map filters, SessionFactoryImplementor factory);
public QueryTranslator createQueryTranslator(String queryIdentifier, String queryString,
Map filters, SessionFactoryImplementor factory);
public QueryTranslator createFilterTranslator(String queryIdentifier, String queryString,
Map filters, SessionFactoryImplementor factory);
調用以上兩個方法只在類 HQLQueryPlan的構造函數中(五個參數的那個)
protected HQLQueryPlan(String hql, String collectionRole, boolean shallow,
Map enabledFilters, SessionFactoryImplementor factory)
{
......
}
protected HQLQueryPlan(String hql, String collectionRole, boolean shallow,
Map enabledFilters, SessionFactoryImplementor factory)
{
......
}
這個構造函數接收你寫的HQL語句還有一個 SessionFactoryImplementor (extends SessionFactory),這個SessionFactory持有hibernate.cfg.xml的配置項HQL語法翻譯器。
讀這個構造函數的代碼,我們發現有兩段代碼
translators[i] = factory.getSettings().getQueryTranslatorFactory()
.createQueryTranslator(hql,concreteQueryStrings[i],enabledFilters, factory );
translators[i] = factory.getSettings().getQueryTranslatorFactory()
.createFilterTranslator(hql, concreteQueryStrings[i], enabledFilters, factory );
translators[i] = factory.getSettings().getQueryTranslatorFactory()
.createQueryTranslator(hql,concreteQueryStrings[i],enabledFilters, factory );
translators[i] = factory.getSettings().getQueryTranslatorFactory()
.createFilterTranslator(hql, concreteQueryStrings[i], enabledFilters, factory );
它們的職能是獲取SessionFactory (hibernate.cfg.xml)所配置的HQL語法分析器,這也就是我們的切入點,我們所希望的事情是:
當構造HQLQueryPlan時,發現傳給的hql是一個Oracle 那樣的THETA-STYLE 連接查詢語句(即像有(+)那樣的語句),我們就繞開在 hibernate.cfg.xml 所配置的AST HQL語法翻譯器,而是采用能夠理解這種語法的傳統的語法翻譯器。
因此我們只要把 HQLQueryPlan類的這個構造函數中的
if ( collectionRole == null) { …………………………… }
改為如下:
if (collectionRole == null) {
// 如果hql語句中使用Oralce式的外連接方式就用傳統的語法翻譯器
if (hql.replaceAll(" s*", "").indexOf("(+)") != -1) {
translators[i] = new ClassicQueryTranslatorFactory().createQueryTranslator(hql,
concreteQueryStrings[i], enabledFilters, factory);
} else {
translators[i] = factory.getSettings().getQueryTranslatorFactory()
.createQueryTranslator(hql, concreteQueryStrings[i], enabledFilters,
factory);
}
translators[i].compile(factory.getSettings().getQuerySubstitutions(), shallow);
} else {
// 如果hql語句中使用Oralce式的外連接方式就用傳統的語法翻譯器
if (hql.replaceAll(" s*", "").indexOf("(+)") != -1) {
translators[i] = new ClassicQueryTranslatorFactory().createFilterTranslator(hql,
concreteQueryStrings[i], enabledFilters, factory);
} else {
translators[i] = factory.getSettings().getQueryTranslatorFactory()
.createFilterTranslator(hql, concreteQueryStrings[i], enabledFilters,
factory);
}
((FilterTranslator) translators[i]).compile(collectionRole, factory.getSettings()
.getQuerySubstitutions(), shallow);
}
if (collectionRole == null) {
// 如果hql語句中使用Oralce式的外連接方式就用傳統的語法翻譯器
if (hql.replaceAll(" s*", "").indexOf("(+)") != -1) {
translators[i] = new ClassicQueryTranslatorFactory().createQueryTranslator(hql,
concreteQueryStrings[i], enabledFilters, factory);
} else {
translators[i] = factory.getSettings().getQueryTranslatorFactory()
.createQueryTranslator(hql, concreteQueryStrings[i], enabledFilters,
factory);
}
translators[i].compile(factory.getSettings().getQuerySubstitutions(), shallow);
} else {
// 如果hql語句中使用Oralce式的外連接方式就用傳統的語法翻譯器
if (hql.replaceAll(" s*", "").indexOf("(+)") != -1) {
translators[i] = new ClassicQueryTranslatorFactory().createFilterTranslator(hql,
concreteQueryStrings[i], enabledFilters, factory);
} else {
translators[i] = factory.getSettings().getQueryTranslatorFactory()
.createFilterTranslator(hql, concreteQueryStrings[i], enabledFilters,
factory);
}
((FilterTranslator) translators[i]).compile(collectionRole, factory.getSettings()
.getQuerySubstitutions(), shallow);
}
改完之後,把編譯後的HQLQueryPlan.class覆蓋到hibernate3.jar包中相應的目錄中即可,或者把這個類放在classes下相應的目錄中,在WEB應用程序中 WEB-INF/classes中的類是優先於jar包中的類先加載。
以上做法兩種有些矛盾的問題也就得到解決了,org.hibernate.Session既可以執行Hibernate3 引入的 delete/update語句,還能夠在 Oracle/Sybase中用(+)外連接方式而不需要配置X對X的連接關系。
下面再介紹一種折中的解決辦法,不知大家注意到沒有,在Hibernate3中的
org.hibernate.SessionFactory的openSession方法返回的是一個
org.hibernate.classic.Session對象,而org.hibernate.classic.Session是繼承自org.hibernate.Session的。
public org.hibernate.classic.Session openSession(Connection connection);
public interface Session extends org.hibernate.Session
而通常我們順應新潮流,是用org.hibernate.Session去引用SessionFactory的方法openSession()的返回值的,於是我們想用 session.delete(sql) 方法時,就把返回的Session實例轉型為 org.hibernate.classic.Session即可。
((org.hibernate.classic.Session)session).delete("from User u where u.name='Unmi'");
如果你也想用原始Session的其他已被擯棄的方法,亦可如此這般做。
當然了,在另一方面要讓Hibernate 能支持 Oracle/Sybase中用(+)外連接方式, 您還是要使
用傳統的語法分析器,他將不能理解新的delete/update語句,很遺憾。
所以為了順應新的潮流的發展,應使用第一種方法。要知道hibernate3中的delete/update語句可比2中的session.delete(hql)方法效率高,hibernate3中直接向數據庫發一個delete語句,而在hibernate2中的delete(hql)方法是需要首先加載對象在刪除,確有些多次一舉,不過又是也有它的道理,update也類此。
在補充一個:hibernate會對 hql 對應的 HQLQueryPlan 進行緩沖的,在類 QueryPlanCache 中處理
HQLQueryPlanKey key = new HQLQueryPlanKey( queryString, shallow, enabledFilters );
HQLQueryPlan plan = ( HQLQueryPlan ) planCache.get ( key );
if ( plan == null ){ ..................}
HQLQueryPlanKey key = new HQLQueryPlanKey( queryString, shallow, enabledFilters );
HQLQueryPlan plan = ( HQLQueryPlan ) planCache.get ( key );
if ( plan == null ){ ..................}
依據queryString(hql)生成key值.