Hibernate 和 JPA 出了什么問題
對于關系型數據的持久化,Hibernate當然是市場上最流行的解決方案。它已有數年的歷史了,而且有數以千計的項目用到了它。最新版本的Hibernate甚至遵循了Sun的Java持久化API(JPA)規范。所以,既然Hibernate可以完成所有事,為什么還要尋找其他方案呢?
我們認為Hibernate與JPA根本沒有它們看起來得那么完美。我們將列舉幾個對Hibernate誤解的原因來解釋它為什么沒有想像中的那么好。
數據模型定義:元數據的問題
所有關系型數據持久化解決方案都需要理解底層數據模型才能工作。Hibernate提供了兩種定義數據模型的方式:XML映射文件和注解。注解是最近引入的以用于簡化對象關系映射,而且注解比XML映射有很多優點。因此我們將不再考慮XML映射轉而關注于注解。但是本文中與注解相關的所有內容都可以用相似的方式通過xml映射實現。
提供給Hibernate的元數據用于告知其保存對象到數據庫中的位置和方式。這些信息可能不只用于Hibernate也要用于應用程序邏輯,所以你可能希望從代碼中訪問它而不是再次提供冗余的信息。一個適當的例子是某一文本域的長度,或者某個域是否是強制性的。例如在一個用戶接口上,你可能需要這個信息用于展示一個表單輸入控件或用于驗證。
通過注解,你可以從代碼中訪問這個信息,但一個特別注解的屬性不能像普通java對象的屬性那樣被直接引用。以下是一個示例:
這是使用注解訪問元數據的方式:
Method field = Employee.class.getDeclaredMethod( "getFirstname", new Class[0] ); javax.persistence.Column col = field.getAnnotation( javax.persistence.Column.class ); int length = col.length();
這段代碼與使用Empire-db的對象模型架構定義效果相同:
int length = mydb.EMPLOYEES.FIRSTNAME.getSize();
(注意EMPLOYEES和FIRSTNAME都是public final修飾的大寫的成員變量,但也可以通過getter進行訪問——具體訪問方式取決于你的代碼和決定)。
注解也有問題的原因不僅在于編譯時安全的缺失與用戶端代碼的復雜性,它也有很多其他問題。持久化注解所提供的元數據通常并不充足。因此你需要像 Hibernate Validator 提供的附加的注解,或你可能想自定義注解,而這些會使你的映射與訪問代碼更加難以閱讀和管理。更不用說在運行時改變你的數據模型是不可能的了。為元數據使用java對象模型要比以上這些方式都簡單易行。既然不用注解可以做得更好那為什么還非要使用它呢?
我們認為注解不應該為應用邏輯提供可能用到的信息。注解應該只用于為代碼提供編譯器優化或代碼文檔這類具體信息。像@Deprecated 和@SuppressWarnings這類注解是可接受的。盡管注解較XML映射文件能更好地與java代碼相結合,但它們的靈活性比普通的interface和類要差得很遠。注解現在是很新酷的,但用得越廣泛你的代碼將會被污染得越嚴重。不要讓該死的注解重蹈該死的XML配置的覆轍。
數據對象定義:泛濫的getter setter
除了元數據,我們也需要在某地存取我們自己的數據。對于Hibernate和JPA這個地方就是(裝備了與相應表各字段對應的成員變量及它的getter setter方法的)JavaBean或POJO。對于大型數據模型這也就意味著很多行的代碼。Hibernate工具可以通過逆向工程自動生成這些代碼。但對于大型成熟的工程你可能會遇到這樣的問題:一旦你手動更改了bean或映射代碼——并且你希望保存這些改動——自動化工具就出問題了。所以通常這些代碼(包括元數據)是通過手工維護的。更糟糕的是,因為這些對象通常用作填充業務對象的DTO(數據傳送對象),你可能會看到用于Java對象間復制屬性值的無數行代碼。所以最好要把這些getter和setter放到哪里呢?
Empire-db的動態bean對于每個實現的實體都只有一個通用的getter和setter方法。我們仍推薦為每個數據庫實體創建一個單獨的數據對象類,這樣類的總數沒有變——雖然當使用一個通用DBRecord對象時這是沒必要的, 但我們推薦這樣做有兩個原因:首先是為了類型安全,因為你希望你的代碼依賴于特定的實體。其次,因為隨著項目的增長,你很可能需要重寫已經存在的方法并實現的新方法。盡管是這樣,但因為少了這些成員變量和它們對應的getter setter方法,你會有相當少的代碼需要維護。另外,如果有必要或為了簡便,你可能要為某字段添加特殊的getter setter。
動態查詢: 查詢的困境
對于一個關系型數據庫,我們希望它可以友好地支持動態查詢,接下來我們看下Hibernate是如何處理動態查詢的。 Hibernate提供兩種方法: Hibernate查詢語言(HQL)和標準的API。 HQL是Hibernate自己的語言,你必須先學習如何使用它。 它可以視為一個支持java編碼映射的SQL方言。 當你嘗試編譯一個復雜的、帶有約束和連接的、有條件查詢語句時,就會發生問題,因為HQL是由不安全的字符串常量拼接成的。 我們認為在一些復雜度比較高的場景,使用HQL編寫的代碼會變得難以維護。 此時,標準的API是更好的選擇,但是這種方法有缺點:靈活性比較低。
但這又出現了另一個問題:通常的編程任務是需要從一個或多個表中選出幾個字段的集合或計算結果。查詢結果可能用于展示給某一用戶,或用于做其他處理。
對此Hibernate的HQL提供了可定義的select子句,原理是提供了一個特殊的結果bean,它持有你所需要的數據,甚至通過string拼接和數值運算這類SQL函數對其進行轉換。但奇怪的這個功能在項目中幾乎沒有被用到過,人們轉而使用全實體bean(full entity bean),這也就意味著從數據庫中加載了很多沒用的屬性。對于實體間關系的解析,Hibernate既可以使用join(餓漢式)加載所有引用到的實體,也可以啟用懶漢模式——查詢每個引用到的對象——有時甚至只為了一個簡單的屬性。所以假設對于有5個屬性的結果bean,實際上該對象不是持有5個屬性,而是5個對象共加載了超過50個屬性。很明顯這不是你所期望的完美方案。
現在的問題是:如果人們沒有使用正確的方法,到底是人的錯還是工具的錯呢?為了找到答案,我問過我自己:如果我在使用Hibernate,我會一直為我的只讀查詢使用結果bean嗎?還是使用全實體bean?
從性能上看我當然要使用結果bean了。但從代碼質量來說我可能不會這樣做。畢竟像這種表達式
filter.append("select new EmployeeResult(employee.employeeId, employee.firstname || ', ' || employee.lastname, employee.gender, employee.dateOfBirth, department.name) ");
根本就不能提高我的代碼質量。除了避免拼寫錯誤,結果bean的構造函數參數也必須要與這段代碼相匹配。因此錯誤只能在運行時被發現,從而使所有錯誤變得更加惡心。并且畢竟——我必須承認——我有時懶得敲這些屬性名,更不用說它們還帶著前綴呢。
最后我可能會根據實體的大小和查詢的性質來使用這兩種方法。但我能理解為什么人們要避開這個高效合理的特性——卻愿意付出高內存消耗與低性能的代價。
而Empire-db卻簡單得多。你只需要明確地轉換和選擇你在結果集中需要的字段,因為這樣花費的代價是較低的。首先你要列出查詢的所有字段,甚至包括轉換函數(通過IDE的代碼補全功能會容易點)。然后你可以通過構造函數或setter將它們自動或手動保存到一個JavaBean中。最重要的是你可以完全無字符串地實現且它是100%編譯時安全的。以下是一個示例:
要構建的最終 SQL (Oracle syntax):
SELECT t2.EMPLOYEE_ID, t2.LASTNAME || ', ' || t2.FIRSTNAME AS NAME, t1.NAME AS DEPARTMENT FROM (DEPARTMENTS t1 INNER JOIN EMPLOYEES t2 ON t2.DEPARTMENT_ID = t1.DEPARTMENT_ID) WHERE upper(t2.LASTNAME) LIKE upper('Foo%') AND t2.RETIRED=0 ORDER BY t2.LASTNAME, t2.FIRSTNAME
使用Empire-db:
SampleDB db = getDatabase(); // Declare shortcuts (not necessary but convenient) SampleDB.Employees EMP = db.EMPLOYEES; SampleDB.Departments DEP = db.DEPARTMENTS; // Create a command object DBCommand cmd = db.createCommand(); // Select columns cmd.select(EMP.EMPLOYEE_ID); cmd.select(EMP.LASTNAME.append(", ").append(EMP.FIRSTNAME).as("NAME")); cmd.select(DEP.NAME.as("DEPARTMENT")); // Join tables cmd.join (DEP.DEPARTMENT_ID, EMP.DEPARTMENT_ID); // Set constraints cmd.where(EMP.LASTNAME.likeUpper("Foo%")); cmd.where(EMP.RETIRED.is(false)); // Set order cmd.orderBy(EMP.LASTNAME); cmd.orderBy(EMP.FIRSTNAME);
我們的結論
事實上,Hibernate是最高級的傳統ORM解決方案之一。不管怎樣,使用ORM的問題在于,它的設計初衷主要是依賴整個實體才能工作。另一方面,關系型數據庫提供了強大的組合、過濾和轉換實體及它們的字段的能力。為了保留這些靈活性,Hibernate提供了各種特性來彌補這些差距。但是為了正確的使用這些特性,你必須做很多抉擇: XML還是注解,HQL還是criteria API,懶裝還是即使獲取等等。Hibernate內部所做的工作,尤其是當它實際執行一次數據庫連接時所做的事情并不總是簡單明了的(如果你把日志級別調到debug - 你就能看到它的復雜性)。當使用和配置恰當的時候,Hibernate也許能工作的很好,但是這需要花費很長時間去了解很多注意事項。尤其是,如果你對Hibernate還不熟悉,那么學習曲線還是很陡峭的。
Hibernate 最缺少的是對編譯期安全性的支持。在使用HQL和criteria API時,你都需要提供字符串化的屬性名稱甚至整個SQL語句 – 這使得每次數據模型的變更都成為一次冒險 – 這個問題只能通過外部的并且是費時的測試來解決。
相反的,Empire-db解決編譯期安全性問題的方法是,基于Java對象模型定義提供一套類型安全的API。任何時候你修改了你的模型定義,你的Java編譯器就會明確的告訴你這次修改影響了哪些代碼。這極大的提高了代碼質量,并減少了測試的數量和耗時。附帶的好處是你的編碼效率也會提高,因為在你構建動態查詢時,你的IDE將會允許你瀏覽所有表、字段甚至SQL函數。
Empire-db is not an ORM solution as you know it. Its focus is clearly on modelling the way relational databases work in Java and not vice versa. Empire-db is passive and does not interfere with your connection and transaction handling – making it easy to integrate and requiring zero configuration. It does not automatically resolve object references but since you select the data exactly as you need it, there is rarely demand for this. Still if you need it, you may simply add a getter with a few simple lines of code. In this case – we believe – that less sometimes is more.
We recommend that if you are not very familiar with SQL and all you need is to store away and reload your POJO's, Hibernate or another JPA implementation is probably the better choice. But if you want to get the most out of SQL and you want to keep full control over when which statements are executed, with all the additional benefits of metadata access and compile-time safety then you really should give Empire-db a go.
Note: If you feel that any of the criticism we made about Hibernate is without reason please let us know. E-mail: