2013年12月31日

GORM custom id 另解

在前篇 使用 Grails 開發應用系統之感 提到 "框"不"框"住的問題, 其中提到不同版本 Grails 的 domain class 用到 transients 的技巧來避免使用 (Hibernate) object id 做為資料庫的 PK。該算是自己一時失查 (此查非彼"察") 呢? 還是才疏學淺?! 就在無意間瀏覧 Blog 時, 看到一篇 GORM custom id mapping (作者: Ilya Sterin) 的技巧才算找到我的答案。

因此, 依樣畫葫蘆做了一個 domain class:
class Employee {
    String id
    String code // 員工編號
    String name // 員工姓名
 
    static constraints = {
        id maxSize: 3
        code maxSize: 3, nullable: false, blank: false, unique: true
        name maxSize: 20
    }

    static mapping = {
        id generator: 'assigned', name: 'code'
    }

    String getId() {
        this.id ?: getCode()
    }
 
    void setCode(code) {
        this.code = code?.toUpperCase() // 大寫
    }
}

在典型的 CRUD controller 中如果有提供  code 的欄位變更, 因設計了 code 來代替 id 欄位, 而 id 負責查詢而 code 負責輸入的情形下, 需要特別處理 save() 與 update() 兩個 methods :
  1. save() 中...
    • unique validation 需要自行處理, 而非讓它發生 org.springframework.dao.DataIntegrityViolationException
  2. update() 中...
    • 須先行以 params.code 查詢是否輸了已經存在的資料了
    • 變更 code 欄位值之前, 先刪掉"舊"的資料 (它不像下 SQL 那般直接 update key value 就好, 參考資訊: Hibernate, alter identifier/primary key )
如下所示:
    @Transactional
    def save(Employee employeeInstance) {
        // ... (略) ...

        try {
            employeeInstance.save flush:true

        } catch (DataIntegrityViolationException dive) {
            employeeInstance.errors.rejectValue('code', 'default.not.unique.message' ,['code', message(code: 'employee.label'), employeeInstance.code] as Object[], message(code: 'default.not.unique.message'))
            employeeInstance.code = params?.id // 還原
            respond employeeInstance.errors, view:'create'
            return
        }

        request.withFormat {
        // ... (略) ...
    }
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    def beforeUpdate(employeeInstance) {
        if (Employee.findByCode(params?.code?.toUpperCase())) {
            employeeInstance.errors.rejectValue('code', 'default.not.unique.message' ,['code', message(code: 'employee.label'), employeeInstance.code] as Object[], message(code: 'default.not.unique.message'))
            employeeInstance.code = params?.id // 還原
            throw new ValidationException(null, employeeInstance.errors)
        }
    }
    @Transactional
    def update(Employee employeeInstance) {
        // ... (略) ...

        if (params?.id != params?.code?.toUpperCase()) {
            try {
                 this.&beforeUpdate(employeeInstance)
            } catch(e) {
                employeeInstance.discard()
            } finally {
                if (employeeInstance.hasErrors()) {
                    respond employeeInstance.errors, view:'edit'
                    return
                }
            }
            // 先刪後增
            employeeInstance.delete()
            employeeInstance.discard()
            employeeInstance.id = null
        }
        employeeInstance.save flush:true // 功能變為 insert 而不是 update 了

        request.withFormat {
        // ... (略) ...
    }
要注意的是, 這是 v2.3.x 的作法: save() 與 update() methods 都冠上了 @Transactional, 而早期 transaction 處理則是交由 service 來做。所以, 為避免先查詢是否已存在的資料時, 發生
org.hibernate.HibernateException: identifier of an instance of ... was altered from ... to ...
因此將它獨立為另一 method: beforeUpdate(), 並宣告為 transaction "NOT_SUPPORTED"。

完成。

ps. 花了不少時間!

2013年12月30日

Java實戰兩三事

多年前的 presentation:

使用 Grails 開發應用系統之感

ihower 的一篇 FAQ 開頭中提到 [Rails 發明人... : Rails Is Not For Beginners], 不知這自問自答式的內容是否也同意了 Rails 發明人的說法?! 但, 卻又像傳教士般的引導閱者進入 RoR 的世界之中?!

lyhcode 的 [程式設計師小心別被框架給「框」住了...] 一篇也提到 [... 如果一位「資深」的 Java 程式設計師,在過去 3-5 年間在專案中應用這些技術,可是不深入瞭解這些框架解決了甚麼問題、用什麼方法實作、底層如何運作以及如何擴充或調校,就很難有真正屬於自己的 Know-how。...], 文中意在說明"用過"(而非"適當運用")一些 framework 時存在的風險。

當個人近幾年發展的一些小案專使用 Grails 時, 也意識到了一點點以上的狀況: 需要溝通並傳達一些基礎與概念, 讓其他同仁放下排斥的意念並願意花時間來接受, 同時也須避免被它給"框"住了。

譬如: Grails 1.1.x 時代, GORM 並不支援 enum, 如果要使用 inList constraints 並活用在 GSP viewer 中, 確實要花一點技巧才能達到; 雖然在 v1.2.1 以後就支援了, 但當下專案驗收卻是不會等人的。

又如: legacy RDB 與 GORM 結合, 其實談的不是 ORM 而是 R-->O mapping; 因為資料庫是 DBA 在管的, 不是 AP 開發人員想怎樣就怎樣, 恣意使用了 object ID 而挷架了 DB。因此, adopt 欄位時定義了諸如下列的 domain class:
class Department {
    String id // 不使用內定的 long type
    String code
//    ...
    String description
 
    static transients = ['code']

//  ...

    static constraints = {
        id(maxSize: 3)
        code(nullable: false, blank: false, size: 2..3, unique: true)
//        ...
    }
 
    static mapping = {
        id generator: 'assigned'

        columns {
            id column: 'code' // code 即 id
//            ...
        }
    }

//    ...

// 以 code 代替 id
    void setCode(String code) {
        this.id = code.toUpperCase() // 轉 upper case
    }

    String getCode() {
        this.id
    }
}

但, 在到了 v2.3.x 之後卻不能正常運作了, 因為 code 欄位並不是真正的欄位 (尚不知是 Hibernate 版本的問題, 還是 Grails 版本的問題); 如果真的無法增加 id 欄位而只能修改的話, 應該會如下:
class Department {
// ...(略)...
 
    static constraints = {
        id nullable: false, unique: true, maxSize: 3
        code blank: false, size: 2..3
//        ...
    }

    static mapping = {
        id column: 'code', generator: 'assigned'
//        ...
    }

 
//    ...(略)...
 
    String getId() {
        this.id ?: '' // 防止 null value 造成 retrieving 問題
    }

    void setCode(String code) {
        setId(this.code = code?.toUpperCase())
    }
}
除了喪失了對 code 欄位進行 unique constraint 作用, 不料輸入相同重複的 code(即id) 之後, 即使在 controller 中加了 rejectValue() 控制:
def save(Department departmentInstance) {
//    ...

    try {
        departmentInstance.save flush:true
    } catch (e) {
        if (ExceptionUtils.getRootCause(e) instanceof NonUniqueObjectException) {
            departmentInstance.errors.rejectValue('code', 'default.not.unique.message',
                ['code', Department.class.name, departmentInstance.code] as Object[], '')
            respond departmentInstance.errors, view:'create'
        }
        return
    }
//    ...
}
仍無法阻止資料的存檔( create失敗 卻變成 update成功 )!
一時之間找不到解法, 腦袋中頓時出現 "框" 住的感覺。

不過, 正如過去長官的提點: 做事情要有中心思想。如果, 不該"重新造輪子"、"站在巨人的肩膀上"... 角度思考來使用 Grails, 理應快速開發客戶所需的應用系統。畢竟它背後有 SpringSouce(spring.io) 及眾多的 user group 在支撐。