在Struts和Hibernate之間搭起橋樑

摘要

Hibernate和struts是當前市面上幾個最流行的開源的庫之一。它們很有效率,是程序員在開發Java企業應用,挑選幾個競爭的庫的首選。雖然它們經常被一起應用,但是Hibernate的設計目標並不是和Struts一起使用,而Struts在Hibernate誕生好多年之前就發佈了。爲了讓它們在一起工作,仍然有很多挑戰。這篇文章點明瞭Struts和Hibernate之間的一些鴻溝,尤其關係到面向對象建模方面。文章也描述瞭如何在兩者間搭起橋樑,給出了一個基於擴展Struts的解決方案。所有的基於Struts和Hibernate構建的Web應用都能從這個通用的擴展中獲益。

在Hibernate in Action(Manning,2004十月)這本書裏,作者Christian Bauer和Gavin King揭示了面向對象世界的模型和關係數據模型,兩個世界的範例是不一致的。Hibernate非常成功地在存儲層(persistence Layer)將兩者粘合在一起。但是領域模型(domain model)(也就是Model-View-Controller的model layer)和HTML頁面(MVC的View Layer)仍然存在不一致。在這篇文章中,我們將檢查這種不一致,並且探索解決的方案。

範例不一致的再發現

讓我們先看一個經典的parent-child關係例子(看下面的代碼):product和category。Category類定義了一個類型爲long的標示符id和一個類型爲String的屬性name。Product類也有一個類型爲long的標示符id和一個類型爲Category的屬性category,表示了多對一的關係(也就是說很多product可以屬於一個Category)





我們希望一個product可以被更改category,所以我們的HTML提供了一個下拉框列出所有Category。





這裏我們看出了兩者的不一致:在Product領域對象裏,category屬性是Category類型,但是ProductForm只有一個類型爲long的categoryId。這種不匹配不但增加了不一致,而且導致了不必要的代碼進行primitive type的標示符和對應的對象之間的轉換。

這種不一致部分是由於HTML Form自己引起的:它只代表了一種關係模型,不能代表面向對象的模型。面向對象和關係模型的不一致在存儲層由對象關係映射(O/RM)解決。但是類似的問題在表示層(view layer)仍然存在。解決的關鍵是讓他們一起無縫地工作。

Struts的功能和侷限

幸運的是,Struts能夠生成和解釋內嵌的對象屬性。Category下拉框可以用Struts page-construction(html) tag library:




我們假設categories是Category對象的一個list。所以現在我們要修改ProductForm,讓它變得更加“面向對象”,我們要修改ProductForm的categoryId,改成類型爲Category的category。這種改變會導致在Product和ProductForm之間複製屬性的工作更加繁瑣,因爲兩者有相同的屬性。





當我們完成剩餘的Struts Action, configuration, validator, jsp, hibernate層後,開始測試,我們馬上在訪問ProductForm.category.id時遇到了NullPointerException。這是預料中的!因爲ProductForm.category還沒有被設置,同時,Hibernate也會將多對一所聯繫的對象引用設爲空(如果database field爲空指)(譯者:這裏指Hiberate將product.category爲Null,如果該Product沒有聯繫到任何category)。Struts要求所有的對象在顯示(生成HTML Form)和傳播(提交HTML FORM)之前被建立。

讓我們看看如何用ActionForm.reset()來架起橋樑。

(並非如此)臭名昭著的Struts ActionForm

在我第一個星期接觸Struts的時候,我最大的一個疑問就是:爲什麼我必須爲Properties, getter方法, setter方法保持幾乎完全相同的兩份copy, 一份在ActionForm Bean, 一份在DomainObject。這個繁瑣的步驟成了Struts社區最主要的抱怨之一。

以我的觀點,ActionForm存在有原因的。首先,它們可以區別於Domain Object因爲他們但當了不同的角色。在MVC模式下,Domain Object是Model層的一個部分,ActionForm是View層的。因爲Webpage的Field和Database的Field可能不一樣,某些特製的轉換是常見的。第二,ActionForm.validate()方法可以定義非常好用的驗證規則。第三,可能有其他的,特定的View行爲,但是又不想在domain layer實現,特別當persistence framework來管理domain object的時候。

提交Form

讓我們來用ActionForm內有的方法-reset()-來解決view和model之間的不一致。這個reset()方法是在ActionForm在被Struts Controller Servlet處理request時候複製ActionForm屬性之前調用的。這個方法最常見的使用是:checkbox必須被顯式地設爲false,讓沒有被選中的checkbox被正確識別。Reset()也是一個初始化用於view rending對象的合適地方。代碼看起來是這樣的:





Struts在使用用戶提交的值填寫ProductForm之前,Struts會調用reset(),這樣category屬性將會被初始化。請注意,你必須檢查category看它是不是null,後面我們會討論這個。

編輯Form

到目前爲止,我們已經解決了form提交時候的問題。但是當我們在生成form頁面的時候呢?Html:select tag也希望有一個非空的引用,所以我們將在form生成頁面之前調用reset()。我們在action類里加入了一行:





我假設讀者已經對action類和Jakarta commons Beanutils包非常熟悉了。CreateOrLoadProduct()建立了一個新的Product實例或者從數據庫裏載入一個已有的實例,具體取決於這個action是建立或者修改Product的。ProductForm被賦值後(譯者:也就是調用PropertyUtils.copyProperties後),productForm.category已經從Product.category複製過來了(譯者:實際上只是複製了category對象引用,並沒有開銷),然後,ProductForm就能用來生成頁面了。我們同時也必須保證:不覆蓋已經被Hibernate載入的對象,所以我們必須檢查(category)是不是爲null。

因爲reset()方法是在ActionForm中定義的,我們可以把上述代碼放入一個superclass,比如CommonEditAction,來處理這些事情:
    



如果你需要一個只讀的Form, 你有兩個選擇: 第一檢查所聯繫的jsp對象是不是null, 第二複製domain對象到ActionForm之後調用Reset()

保存domain對象

我們解決了提交Form和生成Form頁面的問題, 所以Struts可以滿足了。但是Hibernate呢?當用戶選擇了一個null ID option – 在我們的例子中“no category”option- 並且提交form, productForm.category指向一個新建立的hibernate對象,id爲null。當category屬性從ProductForm複製到Hibernate控制的Product對象並且存儲時,Hibernate會抱怨product.category是一個臨時對象,需要在Product存儲前先被存儲。當然,我們知道它是Null,並且不需要被存儲。所以我們需要將product.category置爲Null,然後Hibernate就能存儲Product了(譯者:在這種情況下,數據庫product.category被設成空值)。我們也不希望改變Hibernate的工作方式,所以我們選擇在複製到Domain對象之前清理這些臨時對象,我們在ProductForm中加了一個方法:





我們在copyProperties之前清理掉這些臨時對象,所以如果ProductForm.category只是用來放Null的,則將ProductForm.category置爲Null。然後Domain對象的category也會被設成null:





一對多關係

我還沒有解決Category到Product的一對多關係。我們把它加入到Category的Metadata中:




注意:Hibernate的cascade屬性爲all-delete-orphan表明:Hibernate需要在存儲包含的Category對象時候,自動存儲Product對象。和parent對象一起存儲child對象的情況並不常見,常見的是:分別控制child的存儲和parent的存儲。在我們的例子中,我們可以容易地做到這一點,如果我們允許用戶在同一個html page編輯Category和ProductS。用set表示Products是非常直觀的:





更進一步
我們已經可以察看,編輯,提交forms,並且存儲相關的objects,但是爲所有的ActionForm類定義CleanupEmptyObjects()和reset()方法是個累贅。我們將用一個抽象的ActionForm來完成協助完成這些工作。

作爲通用的實現,我們必須遍歷所有的Hibernate管理的domain對象,發現他們的identifier,並且測試id值。幸運的是:org.hibernate.metadata包已經有兩個Utility類能取出domain對象的元數據。我們用ClassMetadata類檢查這個object是不是Hibernate管理的。如果是:我們把它們的id Value取出來。我們用了Jakarta Commons Beanutils包來協助JavaBean元數據的操作。




爲了讓代碼可讀,我們省略了Exception的處理代碼。

我們的新AbstractForm類從Struts的ActionForm類繼承,並且提供了通用行爲:reset和cleanup多對一關聯對象。當這個關係是相反的話(也就是一對多關係),那麼每個例子將會有所不同,類似在Abstract類裏實現是比較好的辦法。

總結

Struts和Hibernate是非常流行和強大的框架,他們可以有效地相互合作,並且彌補domain模型和MVC視圖(view)之間的差別。這篇文章討論一個解決Struts/Hibernate Project的通用的方案,並且不需要大量修改已經有的代碼。
發佈了674 篇原創文章 · 獲贊 7 · 訪問量 561萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章