天天看點

《Spring 5 官方文檔》16.ORM和資料通路(一)

當開發者建立資料通路應用程式時,spring會為開發者選擇的orm層對應功能進行優化。而且,開發者可以根據需要來利用spring對內建orm的支援,開發者應該将此內建工作與維護内部類似的基礎架構服務的成本和風險進行權衡。同時,開發者在使用spring內建的時候可以很大程度上不用考慮技術,将orm的支援當做一個庫來使用,因為所有的元件都被設計為可重用的javabean元件了。spring ioc容器中的orm十分易于配置和部署。本節中的大多數示例都是講解在spring容器中的來如何配置。

開發者使用spring架構來中建立自己的orm dao的好處如下:

易于測試。spring ioc的模式使得開發者可以輕易的替換hibernate的<code>sessionfactory</code>執行個體,jdbc的<code>datasource</code>執行個體,事務管理器,以及映射對象(如果有必要)的配置和實作。這一特點十分利于開發者對每個子產品進行獨立的測試。

泛化資料通路異常。spring可以将orm工具的異常封裝起來,将所有異常(可以是受檢異常)封裝成運作時的<code>dataaccessexception</code>體系。這一特性可以令開發者在合适的邏輯層上處理絕大多數不可修複的持久化異常,避免了大量的<code>catch</code>,<code>throw</code>和異常的聲明。開發者還可以按需來處理這些異常。其中,jdbc異常(包括一些特定db語言)都會被封裝為相同的體系,意味着開發者即使使用不同的jdbc操作,基于不同的db,也可以保證一緻的程式設計模型。

通用的資源管理。spring的應用上下文可以通過處理配置源的位置來靈活配置hibernate的<code>sessionfactory</code>執行個體,jpa的<code>entitymanagerfactory</code>執行個體,jdbc的<code>datasource</code>執行個體以及其他類似的資源。spring的這一特性使得這些執行個體的配置十分易于管理和修改。同時,spring還為處理持久化資源的配置提供了高效,易用和安全的處理方式。舉個例子,有些代碼使用了hibernate需要使用相同的<code>session</code>來確定高效性和正确的事務處理。spring通過hibernate的<code>sessionfactory</code>來擷取目前的<code>session</code>,來透明的将<code>session</code>綁定到目前的線程。srping為任何本地或者jta事務環境解決了在使用hibernate時碰到的一些常見問題。

內建事務管理。開發者可以通過<code>@transactional</code>注解或在xml配置檔案中顯式配置事務aop advise攔截,将orm代碼封裝在聲明式的aop方法攔截器中。事務的語義和異常處理(復原等)都可以根據開發者自己的需求來定制。在後面的章節中,資源和事務管理中,開發者可以在不影響orm相關代碼的情況下替換使用不同的事務管理器。例如,開發者可以在本地事務和jta之間進行交換,并在兩種情況下具有相同的完整服務(如聲明式事務)。而且,jdbc相關的代碼在事務上完全和處理orm部分的代碼內建。這對于不适用于orm的資料通路非常有用,例如批處理和blob流式傳輸,仍然需要與orm操作共享常見事務。

本節重點介紹适用于所有內建orm技術的注意事項。在16.3hibernate一節中提供了很多關于如何配置和使用這些特性提的資訊。

spring對orm內建的主要目的是使應用階層化,可以任意選擇資料通路和事務管理技術,并且為應用對象提供松耦合結構。不再将業務邏輯依賴于資料通路或者事務政策上,不再使用基于寫死的資源查找,不再使用難以替代的單例,不再自定義服務的注冊。同時,為應用提供一個簡單和一緻的方法來裝載對象,保證他們的重用并且盡可能不依賴于容器。所有單獨的資料通路功能都可以自己使用,也可以很好地與spring的<code>applicationcontext</code>內建,提供基于xml的配置和不需要spring感覺的普通<code>javabean</code>執行個體。在典型的spring應用程式中,許多重要的對象都是<code>javabean</code>:資料通路模闆,資料通路對象,事務管理器,使用資料通路對象和事務管理器的業務服務,web視圖解析器,使用業務服務的web控制器等等。

通常企業應用都會包含很多重複的的資源管理代碼。很多項目總是嘗試去創造自己的解決方案,有時會為了開發的友善而犧牲對錯誤的處理。spring為資源的配置管理提供了簡單易用的解決方案,在jdbc上使用模闆技術,在orm上使用aop攔截技術。

spring的基礎設施提供了合适的資源處理,同時spring引入了dao層的異常體系,可以适用于任何資料通路政策。對于jdbc直連來說,前面提及到的<code>jdbctemplate</code>類提供了包括連接配接處理,對<code>sqlexception</code>到<code>dataaccessexception</code>的異常封裝,同時還包含對于一些特定資料庫sql錯誤代碼的轉換。對于orm技術來說,可以參考下一節來了解異常封裝的優點。

當在dao層中使用hibernate或者jpa的時候,開發者必須決定該如何處理持久化技術的一些原生異常。dao層會根據選擇技術的不同而抛出<code>hibernateexception</code>或者<code>persistenceexception</code>。這些異常都屬于運作時異常,是以無需顯式聲明和捕捉。同時,開發者同時還需要處理<code>illegalargumentexception</code>和<code>illegalstateexception</code>這類異常。一般情況下,調用方通常隻能将這一類異常視為緻命的異常,除非他們想要自己的應用依賴于持久性技術原生的異常體系。如果需要捕獲一些特定的錯誤,比如樂觀鎖擷取失敗一類的錯誤,隻能選擇調用方和實作政策耦合到一起。對于那些隻基于某種特定orm技術或者不需要特殊異常處理的應用來說,使用orm本身的異常體系的代價是可以接受的。但是,spring可以通過<code>@repository</code>注解透明地應用異常轉換,以解耦調用方和orm技術的耦合:

<code>@repository</code>

<code>public class productdaoimpl implements productdao {</code>

<code></code>

<code>// class body here...</code>

<code>}</code>

<code>&lt;beans&gt;</code>

<code>&lt;!-- exception translation bean post processor --&gt;</code>

<code>&lt;bean class="org.springframework.dao.annotation.persistenceexceptiontranslationpostprocessor"/&gt;</code>

<code>&lt;bean id="myproductdao" class="product.productdaoimpl"/&gt;</code>

<code>&lt;/beans&gt;</code>

上面的後置處理器<code>persistenceexceptiontranslationpostprocessor</code>,會自動查找所有的異常轉義器(實作<code>persistenceexceptiontranslator</code>接口的bean),并且攔截所有标記為<code>@repository</code>注解的bean,通過代理來攔截異常,然後通過<code>persistenceexceptiontranslator</code>将dao層異常轉義後的異常抛出。

總而言之:開發者可以既基于簡單的持久化技術的api和注解來實作dao,同時還受益于spring管理的事務,依賴注入和透明異常轉換(如果需要)到spring的自定義異常層次結構。

從spring 5.0開始,spring需要hibernate orm對jpa的支援要基于4.3或更高的版本,甚至hibernate orm 5.0+可以針對本機hibernate session api進行程式設計。請注意,hibernate團隊可能不會在5.0之前維護任何版本,僅僅專注于5.2以後的版本。

開發者可以将資源如jdbc<code>datasource</code>或hibernate<code>sessionfactory</code>定義為spring容器中的bean來避免将應用程式對象綁定到寫死的資源查找上。應用對象需要通路資源的時候,都通過對應的bean執行個體進行間接查找,詳情可以通過下一節的dao的定義來參考。

下面引用的應用的xml中繼資料定義就展示了如何配置jdbc的<code>datasource</code>和<code>hibernate</code>的<code>sessionfactory</code>的:

<code>&lt;bean id="mydatasource" class="org.apache.commons.dbcp.basicdatasource" destroy-method="close"&gt;</code>

<code>&lt;property name="driverclassname" value="org.hsqldb.jdbcdriver"/&gt;</code>

<code>&lt;property name="url" value="jdbc:hsqldb:hsql://localhost:9001"/&gt;</code>

<code>&lt;property name="username" value="sa"/&gt;</code>

<code>&lt;property name="password" value=""/&gt;</code>

<code>&lt;/bean&gt;</code>

<code>&lt;bean id="mysessionfactory" class="org.springframework.orm.hibernate5.localsessionfactorybean"&gt;</code>

<code>&lt;property name="datasource" ref="mydatasource"/&gt;</code>

<code>&lt;property name="mappingresources"&gt;</code>

<code>&lt;list&gt;</code>

<code>&lt;value&gt;product.hbm.xml&lt;/value&gt;</code>

<code>&lt;/list&gt;</code>

<code>&lt;/property&gt;</code>

<code>&lt;property name="hibernateproperties"&gt;</code>

<code>&lt;value&gt;</code>

<code>hibernate.dialect=org.hibernate.dialect.hsqldialect</code>

<code>&lt;/value&gt;</code>

這樣,從本地的jaksrta commons dbcp的<code>basicdatasource</code>轉換到jndi定位的<code>datasource</code>僅僅隻需要修改配置檔案。

<code>&lt;jee:jndi-lookup id="mydatasource" jndi-name="java:comp/env/jdbc/myds"/&gt;</code>

開發者也可以通過spring的<code>jndiobjectfactorybean</code>或者<code>&lt;jee:jndi-lookup&gt;</code>來擷取對應bean以通路jndi定位的<code>sessionfactory</code>。但是,jndi定位的<code>sessionfactory</code>在ejb上下文不常見。

hibernate有一個特性稱之為上下文會話,在每個hibernate本身每個事務都管理一個目前的<code>session</code>。這大緻相當于spring每個事務的一個hibernate<code>session</code>的同步。如下的dao的實作類就是基于簡單的hibernate api實作的:

<code>private sessionfactory sessionfactory;</code>

<code>public void setsessionfactory(sessionfactory sessionfactory) {</code>

<code>this.sessionfactory = sessionfactory;</code>

<code>public collection loadproductsbycategory(string category) {</code>

<code>return this.sessionfactory.getcurrentsession()</code>

<code>.createquery("from test.product product where product.category=?")</code>

<code>.setparameter(0, category)</code>

<code>.list();</code>

除了需要在執行個體中持有<code>sessionfactory</code>引用以外,上面的代碼風格跟hibernate文檔中的例子十分相近。spring團隊強烈建議使用這種基于執行個體變量的實作風格,而非守舊的<code>static hibernateutil</code>風格(總的來說,除非絕對必要,否則盡量不要使用<code>static</code>變量來持有資源)。

上面dao的實作完全符合spring依賴注入的樣式:這種方式可以很好的內建spring ioc容器,就好像spring的<code>hibernatetemplate</code>代碼一樣。當然,dao層的實作也可以通過純java的方式來配置(比如在ut中)。簡單執行個體化<code>productdaoimpl</code>并且調用<code>setsessionfactory(...)</code>即可。當然,也可以使用spring bean來進行注入,參考如下xml配置:

<code>&lt;bean id="myproductdao" class="product.productdaoimpl"&gt;</code>

<code>&lt;property name="sessionfactory" ref="mysessionfactory"/&gt;</code>

上面的dao實作方式的好處在于隻依賴于hibernate api,而無需引入spring的class。這從非侵入性的角度來看當然是有吸引力的,毫無疑問,這種開發方式會令hibernate開發人員将會更加自然。

然而,dao層會抛出hibernate自有異常<code>hibernateexception</code>(屬于非檢查異常,無需顯式聲明和使用try-catch),但是也意味着調用方會将異常看做緻命異常——除非調用方将hibernate異常體系作為應用的異常體系來處理。而在這種情況下,除非調用方自己來實作一定的政策,否則捕獲一些諸如樂觀鎖失敗之類的特定錯誤是不可能的。對于強烈基于hibernate的應用程式或不需要對特殊異常處理的應用程式,這種代價可能是可以接受的。

幸運的是,spring的<code>localsessionfactorybean</code>可以通過hibernate的<code>sessionfactory.getcurrentsession()</code>方法為所有的spring事務政策提供支援,使用<code>hibernatetransactionmanager</code>傳回目前的spring管理的事務的<code>session</code>。當然,該方法的标準行為仍然是傳回與正在進行的jta事務相關聯的目前<code>session</code>(如果有的話)。無論開發者是使用spring的<code>jtatransactionmanager</code>,ejb容器管理事務(cmt)還是jta,都會适用此行為。

總而言之:開發者可以基于純hibernate api來實作dao,同時也可以內建spring來管理事務。

spring團隊建議開發者使用spring聲明式的事務支援,這樣可以通過aop事務攔截器來替代事務api的顯式調用。aop事務攔截器可以在spring容器中使用xml或者java的注解來進行配置。這種事務攔截器可以令開發者的代碼和重複的事務代碼相解耦,而開發者可以将精力更多集中在業務邏輯上,而業務邏輯才是應用的核心。

開發者可以在服務層的代碼使用注解<code>@transactional</code>,這樣可以讓spring容器找到這些注解,以對其中注解了的方法提供事務語義。

<code>public class productserviceimpl implements productservice {</code>

<code>private productdao productdao;</code>

<code>public void setproductdao(productdao productdao) {</code>

<code>this.productdao = productdao;</code>

<code>@transactional</code>

<code>public void increasepriceofallproductsincategory(final string category) {</code>

<code>list productstochange = this.productdao.loadproductsbycategory(category);</code>

<code>// ...</code>

<code>@transactional(readonly = true)</code>

<code>public list&lt;product&gt; findallproducts() {</code>

<code>return this.productdao.findallproducts();</code>

開發者所需要做的就是在容器中配置<code>platformtransactionmanager</code>的實作,或者是在xml中配置<code>&lt;tx:annotation-driver/&gt;</code>标簽,這樣就可以在運作時支援<code>@transactional</code>的處理了。參考如下xml代碼:

<code>&lt;?xml version="1.0" encoding="utf-8"?&gt;</code>

<code>&lt;beans xmlns="http://www.springframework.org/schema/beans"</code>

<code>xmlns:xsi="http://www.w3.org/2001/xmlschema-instance"</code>

<code>xmlns:aop="http://www.springframework.org/schema/aop"</code>

<code>xmlns:tx="http://www.springframework.org/schema/tx"</code>

<code>xsi:schemalocation="</code>

<code>http://www.springframework.org/schema/beans</code>

<code>http://www.springframework.org/schema/beans/spring-beans.xsd</code>

<code>http://www.springframework.org/schema/tx</code>

<code>http://www.springframework.org/schema/tx/spring-tx.xsd</code>

<code>http://www.springframework.org/schema/aop</code>

<code>http://www.springframework.org/schema/aop/spring-aop.xsd"&gt;</code>

<code>&lt;!-- sessionfactory, datasource, etc. omitted --&gt;</code>

<code>&lt;bean id="transactionmanager"</code>

<code>class="org.springframework.orm.hibernate5.hibernatetransactionmanager"&gt;</code>

<code>&lt;property name="sessionfactory" ref="sessionfactory"/&gt;</code>

<code>&lt;tx:annotation-driven/&gt;</code>

<code>&lt;bean id="myproductservice" class="product.simpleproductservice"&gt;</code>

<code>&lt;property name="productdao" ref="myproductdao"/&gt;</code>