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. 花了不少時間!

沒有留言: