Grails 核心優勢之一是 GORM (Grails Object Relational Mapping),約定優於配置讓新建項目極速開發。但面對遺留數據庫:表名不符合複數化、主鍵非 id、字段命名不規範、組合主鍵、無版本/時間戳字段——GORM 默認約定全失效。本文實戰演示如何通過映射配置優雅適配非標準結構。
class Airport {
String iataCode
String name
String city
String country
Double latitude
Double longitude
Date lastUpdated
static mapping = {
// 表名映射
table 'AIRPORTS_LEGACY'
// 主鍵映射:非 id、非自增、組合鍵
id name: 'iataCode', generator: 'assigned'
// 組合主鍵示例:id composite: ['carrierCode', 'flightNumber']
// 版本/時間戳禁用(遺留表無此類字段)
version false
dateCreated false
lastUpdated false
// 或映射到現有字段:version column: 'LAST_UPDATED'
// 字段映射:駝峰屬性 → 下劃線列名
columns {
iataCode column: 'IATA_CODE'
name column: 'AIRPORT_NAME'
city column: 'CITY_NAME'
country column: 'COUNTRY_CODE'
latitude column: 'LAT_DEG'
longitude column: 'LON_DEG'
lastUpdated column: 'LAST_UPDATE_TS'
}
// 關聯映射(如有)
// flights lazy: false, fetch: 'join'
// 緩存策略
cache usage: 'read-only', region: 'airportCache'
// 自定義 SQL 類型
// latitude type: 'double precision'
}
// 約束(可選,用於驗證)
static constraints = {
iataCode blank: false, size: 3..3, unique: true
name blank: false, maxSize: 100
city maxSize: 50
country maxSize: 2
}
}
| 指令 | 用途 | 遺留場景示例 |
|---|---|---|
| table | 指定表名 | table 'TBL_USR' |
| id | 主鍵屬性、生成器、組合鍵 | id name: 'ssn', generator: 'assigned' |
| version | 樂觀鎖字段或禁用 | version false / version column: 'VER' |
| dateCreated / lastUpdated | 時間戳字段或禁用 | dateCreated column: 'CRT_TM' |
| columns | 批量字段-列映射 | columns { email column: 'EMAIL_ADDR' } |
| column | 單字段映射(在屬性上) | String email; static mapping = { email column: 'EMAIL_ADDR' } |
| generator | 主鍵生成策略 | assigned、sequence、identity、uuid、custom |
| cache | 二級緩存配置 | cache usage: 'read-write', region: 'userCache' |
| lazy | 關聯加載策略 | lazy: false (急加載)、lazy: 'proxy' (默認) |
| fetch | 關聯獲取方式 | fetch: 'join' (SQL JOIN)、'select' (默認) |
| type | 自定義 Hibernate Type | type: 'encryptedString', type: 'jsonb' |
| sqlType | DDL 生成時的 SQL 類型 | sqlType: 'varchar2(100)' |
| formula | 衍生屬性(只讀,SQL 片段) | fullName formula: 'FIRST_NAME || \' \' || LAST_NAME' |
// 遺留表:PRIMARY KEY (CARRIER_CODE, FLIGHT_NUM)
class Flight {
String carrierCode
String flightNumber
Date departureTime
String origin
String destination
static mapping = {
table 'FLIGHTS_LEGACY'
id composite: ['carrierCode', 'flightNumber']
version false
columns {
carrierCode column: 'CARRIER_CODE'
flightNumber column: 'FLIGHT_NUM'
departureTime column: 'DEP_TM'
origin column: 'ORIG_AIRPORT'
destination column: 'DEST_AIRPORT'
}
}
// 組合主鍵必須實現 equals/hashCode(GORM 自動生成)
// 查詢示例
// Flight.get(['AA', '123']) // 獲取
// Flight.findByCarrierCodeAndFlightNumber('AA', '123')
}
// backupAirports.groovy - Groovy SQL 導出 XML
import groovy.sql.Sql
def sql = Sql.newInstance(
"jdbc:mysql://localhost/legacy_db?useSSL=false",
"user", "pass", "com.mysql.cj.jdbc.Driver"
)
def airports = sql.rows("SELECT * FROM AIRPORTS_LEGACY")
// 導出 XML
def xml = new groovy.xml.MarkupBuilder()
xml.airports {
airports.each { row ->
airport {
row.keySet().each { col ->
"$col"(row[col])
}
}
}
}
// 寫入文件
new File("airports_backup.xml").write(xml.toString())
sql.close()
// 驗證腳本
def oldSql = Sql.newInstance(oldUrl, oldUser, oldPass, oldDriver)
def newSql = Sql.newInstance(newUrl, newUser, newPass, newDriver)
def oldCount = oldSql.firstRow("SELECT COUNT(*) as c FROM AIRPORTS_LEGACY").c
def newCount = newSql.firstRow("SELECT COUNT(*) as c FROM airport").c // GORM 默認表名
assert oldCount == newCount : "數據量不一致: 舊=$oldCount, 新=$newCount"
// 抽樣對比關鍵字段
def sample = oldSql.rows("SELECT IATA_CODE, AIRPORT_NAME FROM AIRPORTS_LEGACY LIMIT 100")
sample.each { row ->
def gorm = Airport.findByIataCode(row.IATA_CODE)
assert gorm != null : "未找到 IATA_CODE=${row.IATA_CODE}"
assert gorm.name == row.AIRPORT_NAME : "名稱不匹配: GORM=${gorm.name}, 旧=${row.AIRPORT_NAME}"
}
println "遷移驗證通過!"
// application.yml
dataSources:
legacy:
url: jdbc:mysql://legacy-host:3306/legacy_db
driverClassName: com.mysql.cj.jdbc.Driver
username: user
password: pass
reporting:
url: jdbc:postgresql://report-host:5432/report_db
driverClassName: org.postgresql.Driver
username: user
password: pass
// Domain 指定數據源
class Airport {
// ...
static mapping = {
dataSource 'legacy' // 綁定到 legacy 數據源
}
}
// 運行時動態切換
Airport.withDataSource('reporting') {
def reportData = Airport.findAllByCountry('US')
}
Airport.withBatch(1000) { airports.each { it.save() } }Airport.readOnly { Airport.list() } 避免快照開銷cache usage: 'read-only' 針對基數據fetch: 'join' 或 criteria { flights { fetchMode 'join' } }GORM 的強大在於約定與配置的平衡。
面對遺留數據庫:備份先行 → 映射配置逐字段對齊 → 組合主鍵正確建模 → 禁用不存在字段 → 數據驗證對比 → 性能優化。
掌握static mapping閉包,讓 GORM 成為遺留系統改造的利器,而非阻力。