在上個月開篇的 精通 Grails 文章中,介紹了名為 Grails 的新 Web 框架。Grails 結合了最新的實 踐,例如模型-視圖-控制器的關注點隔離和約定優於配置。通過將這些實踐與其中內置的 scaffolding 功能結合,使用 Grails 只需花幾分鐘就能建立並運行起一個 Web 站點。
這篇文章的重點是使用 Grails 可以實現簡化的另一領域:使用 Grail 對象關系映射(Grails Object Relational Mapping,GORM)API 進行持久化。我將首先介紹什麼是對象關系映射器(object- relational mapper,ORM),以及如何創建一對多關系。然後將學習數據驗證(確保應用程序不會出現無 用信息輸入/無用信息輸出(garbage in/garbage out)問題)。然後將看到如何使用 Grails ORM 的領 域特定語言(domain-specific language,DSL),使用 DSL 語句能夠在幕後對普通的舊 Groovy 對象( plain old Groovy objects,POGO)的持久化方式進行微調。最後,將看到能夠輕松地切換到另一個關系 數據庫。任何有 JDBC 驅動程序和 Hibernae 方言的數據庫都受支持。
ORM 定義
關系數據庫出現於 20 世紀 70 年代末,但是軟件開發人員至今依然在尋求有效的方法來存入和取出 數據。當今軟件的基礎並不是多數流行數據庫所使用的關系理論,而是基於面向對象的原則。
為此產生了一整套稱為 ORM 的程序,用來緩解在數據庫和面向對象的代碼之間來回轉移數據的痛苦。 Hibernate、TopLink 和 Java 持久性 API(Java Persistence API,JPA)是處理這一問題的三個流行的 Java API(請參閱 參考資料),不過它們都並不完美。這個問題如此持久(不是故意一語雙關,而是實 情),以至於有了自己專用的術語對象關系阻抗失諧(請參閱 參考資料)。
GORM 是在 Hibernate 上的一層薄薄的 Groovy 層。(我猜 “Gibernate” 不像 “GORM” 那樣容易 上口)。這意味著現有的所有 Hibernate 技巧仍然有用 — 例如,HBM 映射文件和標注得到全面支持 — 但這篇文章的重點是 GORM 帶來的有趣功能。
創建一對多關系
對於將 POGO 保存到數據庫表所面臨的挑戰,很容易被低估。實際上,如果只是將一個 POGO 映射到 一個表,那麼工作相當簡單 —POGO 的屬性恰好映射到表列。但是當對象模型稍稍變復雜一點,例如有兩 個彼此相關的 POGO,那麼事情將很快變得困難起來。
例如,請看上個月 文章 中開始的旅行規劃網站。顯然,Trip POGO 在應用程序中有重要的作用。請 在文本編輯器中打開 grails-app/domain/Trip.groovy(如清單 1 所示):
清單 1. Trip 類
class Trip {
String name
String city
Date startDate
Date endDate
String purpose
String notes
}
清單 1 中的每個屬性都輕松漂亮地映射到 Trip 表中的對應字段。還記得在上一期的文章中說過,在 Grail 啟動時,所有存儲在 grails-app/domain 目錄下的 POGO 都會自動創建對應的表。默認情況下, Grails 使用內嵌的 HSQLDB 數據庫,但是到本文結束時,就能夠使用自己喜歡的其他任意關系數據庫。
旅程中經常要包含飛行,所以還應該創建一個 Airline 類(如清單 2 所示):
清單 2. Airline 類
class Airline {
String name
String url
String frequentFlyer
String notes
}
現在要將這兩個類鏈接起來。為了計劃一個通過 Xyz 航線到芝加哥的旅行,在 Groovy 代碼中的表示 方法與在 Java 代碼中的表示方法相同 — 要在 Trip 類中添加一個 Airline 屬性(如清單 3 所示)。 這個技術稱為對象組合(object composition)(請參閱 參考資料)。
清單 3.在 Trip 類中添加 Airline 屬性
class Trip {
String name
String city
...
Airline airline
}
對於軟件模型來說,這種表示方法非常合適,但是關系數據庫采取的表示方法略有不同。表中的每個 記錄都有一個惟一的 ID,稱為主鍵。向 Trip 表添加一個 airline_id 字段,就能將一個記錄與另一個 記錄鏈接在一起(在這個示例中,“Xyz航線” 記錄與 “芝加哥旅行” 記錄鏈接)。這稱為一對多 關 系:一個航線能夠與多個旅行關聯。(在 Grails 的聯機文檔中,可以找到一對一和多對多關系的示例, 請參閱 參考資料。)
這樣形成的數據庫模式只有一個問題。您可能對數據庫成功地進行了規范化(請參閱 參考資料),但 是現在表中的列與軟件模型就失去了同步。如果將 Airline 字段替換成 AirlineId 字段,那麼實現的細 節(在數據庫中持久化 POGO)就洩漏 到了對象模型。Joel Spolsky 將這種情況稱為 抽象洩漏法則 (Law of Leaky Abstractions)(請參閱 參考資料)。
GORM 有助於緩解抽象洩漏問題,它支持使用對 Groovy 有意義的方式表示對象模型,由 GORM 在幕後 處理關系數據庫的問題。但是正如即將看到的,如果需要,覆蓋默認設置也很容易。GORM 並不是隱藏數 據庫細節的不透明的 抽象層,而是一個半透明的 層 — 它嘗試在不進行用戶干預的情況下執行正確的工 作,但是如果用戶需要對它的行為進行自定義,它也可以提供支持。這樣它就提供了兩方面的好處。
現在已經在 POGO 類 Trip 中添加了 Airline 屬性。要完成一對多關系,還要在 Trip 這個 POGO 中 添加一個 hasMany 設置,如清單 4 所示:
清單 4. 在 Airline 中建立一對多關系
class Airline {
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
}
靜態的 hasMany 設置是個 Groovy 的 hashmap:鍵是 trip;值是 Trip 類。如果要在 Airline 類中 設置額外的一對多關系,那麼可以將逗號分隔的鍵/值對放在方括號內。
現在在 grails-app/controllers 中迅速創建一個 AirlineController 類(如清單 5 所示),這樣 就能看出新的一對多關系的效果:
清單 5. AirlineController class
class AirlineController {
def scaffold = Airline
}
還記得在上一期的文章中說過 def scaffold 的功能是告訴 Grails 在運行的時候動態創建基本的 list()、save() 和 edit() 方法。它還告訴 Grails 動態創建 GroovyServer Page(GSP)視圖。請確保 TripController 和 AirlineController 都包含 def scaffold。如果曾經因為輸入 grails generate- all 在 grails-app/views 中生成過任何 GSP 工件,例如 trip 目錄或者是 airline 目錄,都應該刪除 它們。對於這個示例,需要確保既允許 Grails 動態搭建控制器,又允許它動態搭建視圖。
現在域類和控制器類都已經就位,請啟動 Grails。請輸入 grails prod run-app 在生產模式下運行 應用程序。如果一切正常,應該看到歡迎消息:
Server running. Browse to http://localhost:8080/trip-planner
在浏覽器中,應該看到 AirlineController 和 TripController 鏈接。單擊 AirlineController 鏈 接,填寫 Xyz 航線的詳細信息,如圖 1 所示:
圖 1. 一對多關系:一方
如果不喜歡字段按照字母順序排序,也不用擔心。在下一節就能改變這種方式。
現在新建一個旅程,如圖 2 所示。請注意 Airline 的組合框。添加到 Airline 表的每個記錄都在這 裡顯示。不用擔心 “洩漏” 主鍵 — 在下一節將會看到如何添加更具描述性的標簽。
圖 2. 一對多關系:多方
裸對象
前面剛剛了解了在 Airline POGO 上添加提示(靜態的 hasMany)如何影響表在幕後的創建方式以及 前端生成的視圖。這種使用裸對象 修飾域對象的模式(請參閱 參考資料)在 Grails 中應用得非常廣泛 。將這條信息直接添加到 POGO 內,就消除了對外部 XML 配置文件的需求。所有信息都在一個位置內, 可以顯著提高生產率。
例如,如果想消除顯示在組合框中的主鍵的洩漏,只要在 Airline 類中添加 toString 方法就可以, 如清單 6 所示:
清單 6. 在 Airline 中添加 toString 方法
class Airline {
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
String toString(){
return name
}
}
從現在開始,在組合框中顯示的值就是航線的名稱。這裡真正酷的地方在於:如果 Grail 依然在運行 ,那麼只要保存 Airline.groovy,修改就會生效。請在浏覽器中新建一個 Trip,看看這樣做的效果。因 為視圖是動態生成的,所以能夠迅速地在文本編輯器和浏覽器之間來回切換,直到看到合適的視圖 — 不 需要重新啟動服務器。
現在我們來解決字段按字母順序排序的問題。要解決這個問題,需要向 POGO 添加另一個配置: static constraints 塊。請按清單 7 所示的順序將字段添加到這個塊(這些約束不影響列在表中的順序 — 只影響在視圖中的順序)。
清單 7. 修改 Airline 中的字段順序
class Airline {
static constraints = {
name()
url()
frequentFlyer()
notes()
}
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
String toString(){
return name
}
}
將修改保存到 Airline.groovy 文件,在浏覽器中新建一個航線。現在裡面的字段應該按照在清單 7 中指定的順序出現,如圖 3 所示:
圖 3. 自定義的字段順序
在您准備責備我沒有必要在 POGO 中輸入兩次字段名稱而違背 DRY 原則(不要重復你自己)時(請參 閱 參考資料),請稍等一下,因為將它們放在獨立的塊內有很好的理由。清單 7 的 static constraints 塊內的大括號不會總是空白。
數據驗證
除了指定字段順序, static constraints 塊還允許在裡面放置一些驗證規則。例如,可以在 String 字段上施加長度限制(默認是 255 個字符)。這樣就能確保 String 值與指定的模式(例如電子郵件地 址或 URL)匹配。甚至還能將字段設置為可選或必需的。關於可用的驗證規則的完整列表,請參閱 Grails 的聯機文檔(請參閱 參考資料)。
清單 8 顯示的 Airline 類中在約束塊內添加了驗證規則:
清單 8. 將數據驗證添加到 Airline
class Airline {
static constraints = {
name(blank:false, maxSize:100)
url(url:true)
frequentFlyer(blank:true)
notes(maxSize:1500)
}
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
String toString(){
return name
}
}
保存修改後的 Airline.groovy 文件,在浏覽器中新建一條航線。如果違反了驗證規則,會收到警告 ,如圖 4 所示:
圖 4. 驗證警告
可以在 grails-app/i18n 目錄的 messages.properties 文件中對警告消息進行自定義。請注意,默 認的消息已經用多種語言進行了本地化(請參閱 Grail 聯機文檔中的驗證一節,了解如何在每個類、每 個字段的基礎上創建自定義消息)。
清單 8 中的多數約束只影響視圖層,但是有兩個約束也會影響持久層。例如,數據庫中的 name 列現 在是 100 個字符長。notes 字段除了從輸入字段轉為視圖的文本區域之外(對於大於 255 個字符的字段 會進行這個轉換),還從 VARCHAR 列轉為 TEXT、CLOB 或 BLOB 列。這些轉變取決於在後台使用的數據 庫類型和它的 Hibernate 方言 — 當然,這些也是可以修改的。
Grails ORM 的 DSL
可以使用任何常用的配置方法覆蓋 Hibernate 的默認設置:HBM 映射文件或者標注。但是 Grails 提 供了第三種方式,這種方式采用了裸對象的形式。只要向 POJO 添加一個 static mapping 塊,就能覆蓋 默認的表和字段名稱,如清單 9 所示:
清單 9. 使用 GORM DSL
class Airline {
static mapping = {
table 'some_other_table_name'
columns {
name column:'airline_name'
url column:'link'
frequentFlyer column:'ff_id'
}
}
static constraints = {
name(blank:false, maxSize:100)
url(url:true)
frequentFlyer(blank:true)
notes(maxSize:1500)
}
static hasMany = [trip:Trip]
String name
String url
String frequentFlyer
String notes
String toString(){
return name
}
}
如果要在新的 Grails 應用程序中使用現有的遺留表,那麼這個映射塊會特別有幫助。雖然這裡只介 紹了點皮毛,但 ORM DSL 提供的功能遠不止是重新映射表和字段的名稱。每個列的默認數據類型都可以 覆蓋。可以調整主鍵的生成策略,甚至指定復合主鍵。可以修改 Hibernate 的緩存設置,調整外鍵關聯 使用的字段,等等。
要記住的要點是所有這些設置都集中在一個地方:POGO 內。
理解 DataSource.groovy
目前所做的工作都集中在單個類的調整上。下面我們要回過頭來做一些全局性的修改。所有域類共享 的特定於數據庫的配置保存在一個公共文件內:grails-app/conf/DataSource.groovy,如清單 10 所示 。請將這個文件放在一個文本編輯器內仔細查看:
清單 10. DataSource.groovy
dataSource {
pooled = false
driverClassName = "org.hsqldb.jdbcDriver"
username = "sa"
password = ""
}
hibernate {
cache.use_second_level_cache=true
cache.use_query_cache=true
cache.provider_class='org.hibernate.cache.EhCacheProvider'
}
// environment specific settings
environments {
development {
dataSource {
dbCreate = "create-drop" // one of 'create', 'create-drop','update'
url = "jdbc:hsqldb:mem:devDB"
}
}
test {
dataSource {
dbCreate = "update"
url = "jdbc:hsqldb:mem:testDb"
}
}
production {
dataSource {
dbCreate = "update"
url = "jdbc:hsqldb:file:prodDb;shutdown=true"
}
}
}
在 dataSource 塊內能夠修改用來連接數據庫的 driverClassName、username 和 password。 hibernate 塊用來調整緩存設置(除非是 Hibernate 專家,否則不要在這裡進行任何調整)。真正有意 思的是 environments 塊。
還記得在上一期的文章中介紹過 Grails 能夠在三種模式下運行:開發模式、測試模式和生產模式。 在輸入 grails prod run-app 時,就是告訴 Grails 使用 production 塊中的數據庫設置。如果希望根 據環境調整 username 和 password 的設置,只要將這些設置從 dataSource 塊復制到每個 environment 塊,並修改設置的值即可。 environment 塊中的設置覆蓋 dataSource 塊中的設置。
url 設置是 JDBC 的連接字符串。請注意在 production 模式下,HSQLDB 使用基於文件的數據存儲。 在 development 和 test 模式下,HSQLDB 使用內存中的數據存儲。上個月我介紹過如果想讓 Trip 的記 錄在服務器重新啟動之後保留,應該在 production 模式下運行。現在您應該知道如何在 development 和 test 模式下進行設置以實現這一功能 — 只要將 url 設置從 production 復制過來即可。當然,將 Grails 指向 DB2、MySQL 或者其他傳統的基於文件的數據庫也可以解決記錄消失的問題(立刻就會介紹 DB2 和 MySQL 的設置)。
dbCreate 的值在不同的環境下會產生不同的行為。它是底層的 hibernate.hbm2ddl.auto 設置的別名 ,負責指定 Hibernate 在幕後如何管理表。將 dbCreate 設為 create-drop,就是告訴在啟動的時候創 建 表,在關閉的時候刪除 表。如果將值改為 create,那麼 Hibernate 會在需要的時候創建新表和修改 現有表,但是重新啟動之間的所有記錄都會被刪除。production 模式的默認值 — update — 會在重新 啟動之間保持所有數據,也會在需要的時候創建或修改表。
如果對傳統的數據庫使用 Grails,那麼我強烈推薦注釋掉 dbCreate 的值。這樣就告訴 Hibernate 不要觸及數據庫的模式。雖然這意味著必須自行保持數據模型與底層數據庫同步,但這可以大大減少憤怒 的 DBA 為了弄清楚誰在未經允許的情況下不斷修改數據庫表而發來的質問郵件。
添加自定義環境也很容易。例如,公司中可能有一個 beta 程序。只要在 DataSource.groovy 中其他 塊之後創建一個 beta 塊即可(也可以針對與數據庫無關的設置在 grails-app/conf/Config.groovy 中 添加一個 environments 塊)。要在 beta 模式下啟動 Grails,請輸入 grails -Dgrails.env=beta run-app。
修改數據庫
如果通過 dbCreate 設置允許 Hibernate 管理表,那麼只需三步就能迅速地將 Grails 指向新表:創 建數據庫並登錄,將 JDBC 驅動程序復制到 lib 目錄,調整 DataSource.groovy 中的設置。
對於不同的產品,創建數據庫和用戶的操作過程有很大差異。對於 DB2 來說,可以按照一份聯機的詳 細教程逐步進行(請參閱 參考資料)。創建了數據庫和用戶之後,請調整 DataSource.groovy,讓它使 用清單 11 中的值(這裡顯示的值假設使用的數據庫名為 trip)。
清單 11. DataSource.groovy 的 DB2 設置
driverClassName = "com.ibm.db2.jcc.DB2Driver"
username = "db2admin"
password = "db2admin"
url = "jdbc:db2://localhost:50000/trip"
如果安裝了 MySQL,那麼請使用清單 12 所示的步驟登錄為 root 用戶,並創建 trip 數據庫:
清單 12. 創建 MySQL 數據庫
$ mysql --user=root
mysql> create database trip;
mysql> use trip;
mysql> grant all on trip.* to grails@localhost identified by 'server';
mysql> flush privileges;
mysql> exit
$ mysql --user=grails -p --database=trip
創建了數據庫和用戶之後,請調整 DataSource.groovy,讓它使用清單 13 所示的值:
清單 13. DataSource.groovy 的 MySQL 設置
driverClassName = "com.mysql.jdbc.Driver"
username = "grails"
password = "server"
url = "jdbc:mysql://localhost:3306/trip?autoreconnect=true"
創建了數據庫,將驅動程序 JAR 復制到 lib 目錄,而且調整了 DataSource.groovy 中的值之後,多 次輸入 grails run-app。現在的 Grails 使用的就是 HSQLDB 之外的數據庫。
結束語
現在對本期的 GORM 介紹做一小結。通過本文,您應該很好地理解了什麼是 ORM、如何管理驗證和表 關系以及如何用自己選擇的數據庫替換 HSQLDB。
這個系列的下一篇文章將重點放在 Web 層上。在下篇文章中將學習 GSP 的更多內容以及各種 Groovy TagLib。還將看到如何將 GSP 拆分成多個部分 — 即能夠在多個頁面上重用的標記片段。最後,還將學 會如何自定義在搭建的視圖中使用的默認模板。