Grails 2.0 倒計時:資料庫遷移

工程 | Peter Ledbrook | 2011 年 8 月 17 日 | ...

Grails 眾多優秀特性之一就是它能根據你的領域模型自動建立資料庫 schema。誠然,這是 Grails 使用的 Hibernate 的一個特性,但這仍然能幫助你快速啟動資料庫驅動的 Web 應用,而無需擔心資料庫 schema 的問題。

當你的應用部署到生產環境後會發生什麼?在開發過程中,伺服器執行之間丟失資料不是大問題。但在生產環境中,你不能直接刪除資料庫。因此,資料庫資料來源設定的“create”和“create-drop”值就被排除了。那“update”呢?它不會清除資料庫中的資料,所以經常被大家使用。然而,它在生產環境中效果不佳,因為它存在明顯的限制。例如,它無法處理簡單的列重新命名,當然也無法處理對現有資料的修改,而這些修改可能是升級的必要部分。儘管在生產部署中使用dbCreate設定為“update”很有誘惑力,但這通常是錯誤的解決方案。dbCreate = "update"那該怎麼辦呢?使用 SQL 指令碼來執行遷移?這當然可行,但為給定的 GORM 領域模型變更建立適當的 SQL 指令碼並不容易。如果你需要支援多種資料庫型別,那這種方法也不合適,因為每種資料庫的 SQL 可能會不同。

所剩的選擇是什麼?用 SQL 指令碼來執行遷移?這當然可以,但要為你的 GORM 領域模型中的給定變更建立相應的 SQL 並不容易。而且如果你需要支援多種資料庫型別,這也不合適,因為每種資料庫的 SQL 可能都不同。

幸運的是,有一個靈活的、與資料庫無關的工具用於執行 schema 遷移:Liquibase。更好的是,有兩個 Grails 外掛可以比直接使用 Liquibase 更方便:Liquibase 外掛Autobase

那麼,既然這些外掛已經存在很長時間了,Grails 2.0 有什麼新變化呢?資料庫遷移是 Grails 用於任何嚴肅工作的重要組成部分,因此 Grails 團隊決定應該有一個官方的方式來處理它們。結果是出現了一個新外掛,它結合了 Liquibase 和 Autobase 外掛的最佳部分:資料庫遷移外掛

簡而言之

儘管核心資料庫遷移支援是 Grails 2.0 路線圖的一部分,但沒有理由將其繫結到該版本。這意味著你也可以在 Grails 1.3 專案上使用該外掛。該外掛為你的應用帶來了這些特性:

  • 宣告式資料庫 schema 和資料遷移
  • Groovy 和 XML 遷移指令碼
  • 手動或自動執行遷移
  • 自動跟蹤已應用的遷移
  • 透過對比領域模型和資料庫生成遷移

它提供了 20 多個命令,給你充足的控制權。你可以從外掛的使用者指南中瞭解更多資訊,但簡單來說,它為管理 Grails 應用新版本的資料庫受控升級提供了極大的幫助。如果你願意,仍然可以使用其他外掛甚至完全不同的方法,但對大多數使用者來說,這絕對是推薦的方式。

本文的其餘部分將向你展示該外掛的一個常見使用模式。

入門

想象一下,你一直在努力開發一個 Grails 應用,現在你想將第一個版本部署到生產環境。是時候考慮如何管理資料庫升級了。此時,生產資料庫甚至還沒有建立。因此,將資料庫遷移外掛宣告為執行時依賴項,如下所示:

grails.project.dependency.resolution = {
    inherits "global"
    ...
    plugins {
        runtime ":database-migration:1.0"

        compile ":hibernate:$grailsVersion"
        compile ":jquery:1.6.1.1"
        compile ":resources:1.0.2"

        build ":tomcat:$grailsVersion"
    }
    ...
}

然後執行grails compile。在撰寫本文時,1.0 是外掛的最新版本。請檢視 Grails 外掛門戶以瞭解任何時候的最新版本。

外掛可用後,你就可以開始資料庫遷移之旅了。正如我所說,生產資料庫尚未建立。你可以部署應用的初始版本,使用dbCreate設定為“update”,這也能正常工作。但出於我稍後討論的原因,我希望鼓勵你從資料庫遷移 changelog(即遷移指令碼)初始化資料庫。別擔心,這比你想象的要省事得多。

值為“update”並且執行良好。但由於我稍後會討論的原因,我希望鼓勵你從資料庫遷移 changelog(即遷移指令碼)來初始化資料庫。別擔心,這比你想象的要省事得多。dbCreate設定移除。然後執行

grails dbm-create-changelog
grails prod dbm-generate-gorm-changelog --add changelog-1.0.groovy

(現在是嘗試新的 Grails 2 互動模式的好時機!)

上述命令將建立一個grails-app/migrations/changelog.groovy檔案,它將成為你的父 changelog 檔案。第二個命令然後生成一個遷移指令碼,grails-app/migrations/changelog-1.0.groovy,該指令碼將針對空資料庫建立與當前應用版本對應的 schema。父 changelog 也會更新以包含這個新的 changelog。請注意,你的資料庫將保持為空!

雖然 Liquibase 旨在與資料庫無關,但最好在配置了適當型別資料來源(通常是“production”)的 Grails 環境中執行各種生成和 diff 命令。這將確保你無需對生成的 changelog 進行太多更改。

[提示 標題=Changelog 名稱] 外掛沒有強制要求你對 changelog 檔名採用任何特定的命名約定。在本文中,我只是使用了 'changelog-',因此每個 changelog 都與應用的特定版本相關聯。這種方式效果不錯。[/提示]

我們為什麼要建立 1.0 的 changelog 而不是使用“update”dbCreate設定呢?最初的 changelog 意味著你可以將你的應用部署到一個全新的、空的資料庫。所有遷移都將按預期工作,因為它們將從一個已知、固定的 schema(即一個空的 schema)按順序執行。“update”設定的問題在於,它總是會建立一個與當前領域模型匹配的 schema。較舊的 changelog 根本無法執行,因為資料庫的狀態不是它們所期望的!如果你願意,你的第一個應用版本仍然可以使用“update”,但上述方法意味著你可以在早期測試你的初始 changelog。

此時,值得回顧一下changelog-1.0.groovy檔案,看看遷移是什麼樣子,以及是否有需要更改的地方。你可能會看到添加了比實際需要更多的索引和約束,因此可能需要進行一些刪減。

好的,我們現在有一個遷移指令碼了。但是當我們執行grails prod run-app時,應用不會啟動:資料庫仍然是空的。為什麼?遷移指令碼不會在啟動時自動執行,這意味著你必須手動使用dbm-update這樣的命令更新資料庫,或者在啟動時啟用遷移執行。我更喜歡後者,特別是在許多情況下你無法從 Grails 命令列訪問生產資料庫。因此,將以下設定新增到Config.groovy檔案中:

grails.plugin.databasemigration.updateOnStart = true
grails.plugin.databasemigration.updateOnStartFileNames = ["changelog.groovy"]

現在當你啟動應用時,遷移指令碼將會執行,你的應用也將工作!當你重啟伺服器時,外掛會檢測到遷移已經執行過,所以它們會被忽略。

後續變更

資料庫遷移的全部意義在於,隨著應用的演變,你會對資料庫進行變更並控制這個過程。所以現在想象一下,你已經對應用做了一些工作,並想釋出一個新版本,比如 1.0.1。要升級生產資料庫,你需要為你的領域模型變更建立一個 changelog。你可以手動完成,但利用資料庫遷移外掛的“diff”命令可以省下不少力氣。

要執行 diff,你需要一個外掛可以用來與當前領域模型進行比較的資料庫。同樣,使用與生產環境相同型別的資料庫是個好主意。此外,資料庫必須處於其原始狀態,即在當前領域模型變更發生之前。換句話說,不要對資料庫執行dbCreate = "update"啟用的應用程式!事實上,為每個應用版本建立資料庫轉儲是值得的,這樣萬一你不小心以這種方式更新了資料庫,還可以回滾。

好了,警告到此為止。讓我們建立下一個 changelog:

grails prod dbm-gorm-diff --add changelog-1.0.1.groovy

和之前一樣,這將建立一個changelog-1.0.1.groovy檔案在 migrations 目錄下,並從父 changelog 中包含它。你還需要檢查生成的 changelog 並可能進行調整,但編輯現有檔案比從頭建立一個新檔案要容易得多。就這樣!現在你可以將這個 changelog 和相應的領域類變更一起提交到版本控制。事實上,我建議你總是將領域類變更和相應的遷移指令碼修改放在同一個提交中。

changelog 檔案本身看起來像這樣:

databaseChangeLog = {
    changeSet(id: "UpdateDescriminatorForPluginTabs", author: "pledbrook") {
        update(tableName: "content") {
            column name: "class", value: "org.grails.plugin.PluginTab"
            where "title like 'description-%' and class = 'org.grails.wiki.WikiPage'"
        }
    }

    changeSet(id: "IssuesUrlForPlugins", author: "pledbrook") {
        addColumn(tableName: "plugin") {
            column name: "issues_url", type: "varchar(255)", {
                constraints nullable: true
            }
        }
    }
}

如你所見,它們只是變更集的集合,每個變更集包含一些資料庫重構。每個變更集在每個 changelog 中都需要一個作者唯一的 ID,以便 Liquibase 可以跟蹤它是否已應用(Liquibase 跟蹤的是變更集,而不是 changelog!)。上面的例子展示瞭如何更新現有資料(ID 為 "UpdateDescriminatorForPluginTabs")以及新增一個新列。其他支援的重構包括:

  • 新增/重新命名/修改列(schema 變更)
  • 新增索引
  • 新增/重新命名/修改表
  • 新增/移除唯一約束

完整的重構範圍在 Liquibase 手冊中有所描述,儘管所有示例都是 XML。幸運的是,XML -> Groovy 的對映相當直接:

  1. XML 元素 -> Groovy 方法
  2. 屬性 -> 命名引數
  3. 巢狀元素 -> 閉包內的巢狀方法

最後,你可能想知道如何組織你的 changelog。是每個重構一個變更集?還是每個 changelog 一個變更集?或者介於兩者之間?這取決於你,但每個原始碼控制提交對應一個變更集效果不錯。換句話說,對於包含領域模型變更的每個提交,你建立一個變更集。或者,你可能希望每個資料庫表對應一個變更集。做你認為合適的方式。

本文到此為止。正如你所見,適當的資料庫遷移支援對於任何生產資料庫支援的 Web 應用都是重要的一部分,因此我們現在擁有一個官方支援且功能強大的外掛來滿足這一需求是個好訊息。它僅適用於關係型資料庫(所以對於 Redis、MongoDB 等,恐怕沒有遷移支援),但它應該仍然能夠滿足絕大多數 Grails 使用者的需求。快去試試吧!

獲取 Spring 新聞簡報

與 Spring 新聞簡報保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助你飛速發展。

瞭解更多

獲取支援

Tanzu Spring 在一份簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

即將舉辦的活動

檢視 Spring 社群所有即將舉辦的活動。

檢視全部