首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Grails GORM 實戰:靈活映射遺留數據庫非標準表結構

     編輯:Java 相關知識

前言:遺留系統改造的核心挑戰

Grails 核心優勢之一是 GORM (Grails Object Relational Mapping),約定優於配置讓新建項目極速開發。但面對遺留數據庫:表名不符合複數化、主鍵非 id、字段命名不規範、組合主鍵、無版本/時間戳字段——GORM 默認約定全失效。本文實戰演示如何通過映射配置優雅適配非標準結構。

一、核心映射配置:static mapping 閉包


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 Typetype: 'encryptedString', type: 'jsonb'
sqlTypeDDL 生成時的 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')
}

四、數據遷移與備份策略

4.1 遷移前必備:數據備份腳本


// 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()

4.2 遷移驗證:對比新舊數據


// 驗證腳本
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() } 避免快照開銷
  • 二級緩存:配合 Ehcache/Redis,配置 cache usage: 'read-only' 針對基數據
  • FetchProfile:定義命名獲取策略,按場景激活
  • 避免 N+1:關聯查詢使用 fetch: 'join' 或 criteria { flights { fetchMode 'join' } }

七、總結

GORM 的強大在於約定與配置的平衡。
面對遺留數據庫:備份先行 → 映射配置逐字段對齊 → 組合主鍵正確建模 → 禁用不存在字段 → 數據驗證對比 → 性能優化。
掌握 static mapping 閉包,讓 GORM 成為遺留系統改造的利器,而非阻力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved