顯示具有 Grails 標籤的文章。 顯示所有文章
顯示具有 Grails 標籤的文章。 顯示所有文章

2014年1月12日

Grails Scaffolding

Grails 的 Scaffolding 功能自開天闢地以來就有了, 也是介紹入門時的介紹重點之一。

上篇 GORM custom id 另解 就是由 scaffolding plugin 產生出來後所進行的調整; 如果多數 controller 變更情形如同上篇, 那多半會考慮變更 template files, 在產生(generate-all)時就大致已調整完成, 而稍做修改即可。

變更修改前, 須先產生預設的 template files 在專案目錄 src/templates(/scaffolding) 下:
grails> install-templates
接著再以 editor 直修改它, 存檔後會在下次產生另一組 controller/views 時生效。

其實這小撇步早在多年前另一篇 GORM 物件於 update method 後立即顯示資料 也用過。

當時 Grails 的版本及 IDE 工具比較沒有整合的很好, 多半是 console 下打指令來進行開發 (而 IDE 則用來 debug)。目前的版本 v2.3 早已提供 interactive mode, 可利 TAB 鍵進行快速提示與執行; 另外 GGTS IDE 也提供了很好的整合, 以 generate-all 指令為例, 有兩種方式:
(1) 原先提供的 context menu :
  • 在 project 上按右鍵, 選擇 [Grails Tools]
  • 選擇適合的功能 (多半以 [Open Grails Command Prompt])
  • 當然, 最快的方式是按下快捷鍵 [Cmd]+[Opt]+[Shift]+G

(2) [New] function:
  • 直接在 domain class 上按右鍵, 選 [New]
  • 選擇 [Generate Controller and Views], 並輸入 domain class 即可
以上

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日

使用 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 在支撐。

2011年12月23日

在 STS 中整合 Grails 與 Scala

目前看到新發佈的 Grails v2.0.0,馬上就試著加到 STS (SpringSource Tool Suite)中;但心想 open source 界熱門話題的 Scala 剛推出 v2.0,也一併來用用看。
於是在一個練習的 grails project 安裝了 scala plugin (v0.6.4),接著在 src/scala 中寫了一個 .scala ;不過 run-app 時出現了 ...

[scalaPlugin] Compiling Scala sources to target/classes
 | Error Error executing script Compile:
 : Could not compile Scala sources: BuildException:
 Compile failed because of an internal compiler error (object scala not found.);
 see the error output for details. (Use --stacktrace to see the full trace)

而網路上已有了解決的方法(如下圖所示),顯然這不構成問題。






但試想:每每得先 grails compile 出 .class 才能做單元測試,試乎沒用到 IDE 中 scala builder 的好處。所以,我將這個 project 加上 builder:

 org.scala-ide.sdt.core.scalabuilder
 

與 nature :
org.scala-ide.sdt.core.scalanature
而此舉引發了兩個的問題:
IDE 會出現要求加入 scala library 的錯誤訊息。
但加了 lib 之後,則又引起了 grails builder 的編譯錯誤。 

所以,我改了方式: 保持這個 grails project 的原樣,另起一個 scala project;
而這個新 project 的 src 目錄以 link folder 的形式連接到 grails project 的 src/scala。 如下圖所示:




























這樣子就能寫 .scala 時做單元測試,而又不影響 grails project 的任何操作了。

2011年5月4日

Grails project 發佈時 UnsupportedClassVersionError 問題

經歴過 Grails v1.0.4 / v1.0.1 / v1.3.7 等版本, 也用過 Eclipse + Groovy plugin 工具; 對於開發軟體的過程而言, 並不會跳脫指令的使用。像是 create-controller ... 等, 這不過是 terminal 模式與 [External Tools] 的 launching 方式不同。當然, 這些是環境變數 JAVA_HOME、GRAILS_HOME ... 等(範疇), 在工具外或工具內的應用而已。

對於寫過 Eclipse plugin 的人, 像是 STS v2.5.x 這樣的工具, 就像是把部份的指令模式做成 context menu; 用起來當是比先前用 [External Tools Configuration] 方便許多; 但是為解決 "linked resource" 問題, SpringSource.com 將功能做在 v2.6.0 版本之中。雖然問題克服了, 但相對上使用這樣更新一版的 IDE 工具, 無形之中已經改用了不同版本的 groovy ; 可能存在著一些編譯的風險跟問題。

除此之外, 在新版的 Grails 中, 編譯 script 用到不同版本的 ANT; 對於已熟悉 Eclipse 的設定、或慣用 ANT script 的人來說, 控制編譯的版本 (source / target options), 似乎不能一下了解新版的運作情形。最常見的就是使用 Java 6 的 JDK (通常會去控制 Eclipse / project 設定 / ANT build file 等), 經過工具的 packing 並 deploy 至 Java 5 run-time 環境時, 會發生 UnsupportedClassVersionError !

解決的方法, 如同這篇的敍述, 是去變更 grails 安裝目錄下的 build.properties。

但, 我們所知道的是, ANT 這樣子的工具是支援 system properties 的; 也就是說, 可以在進行編譯 script 時加 VM options , 就可以變更 system properties。所以, 可以在 STS v.2.6.0 的 context menu 操作時輸入相關 options, 如下所示:







不過試過之後, 並沒有發生作用。也許之前的版本可以, 但沒有多餘的時間去測試了。

2010年6月18日

為 Grails service 物件宣告 logger

一般來說, Java class 中使用 logger(以 Log4j 為例), 會使用兩個方式:
private static final Logger log = Logger.getLogger(MyClass.class);

private static final Logger log = Logger.getLogger("MyClass");
而在 Grails 的 controller 中則可以直接使用 impicit 物件: log
主要是因為 Grails 已經幫忙做了 Dependency Injection; 而其 logger 物件已在 conf/Config.groovy 宣告

但是, service 物件怎麼辦呢?
此時的做法, 則會像一般 Java class 的方式(以 SLF4J 為例)來宣告, 如下:
private static final Logger log = LoggerFactory.getLogger(MyService.class)
同時, 規劃了 log 的輸出檔案; 也就是在 conf/Config.groovy 中設計一個 appender:
log4j = {
  appenders {
      appender new org.apache.log4j.DailyRollingFileAppender(
        name: "dailyAppender",
        layout: pattern(conversionPattern: '[%d{yyyy-MM-dd HH:mm:ss}] %p %m%n'), 
        file: "${System.properties['java.io.tmpdir']}/my-test.log",
        datePattern: "'.'yyyy-MM-dd")
  }
...
  info  dailyAppender: 'MyService'
...
但, 相關訊息輸出的執行結果未出現在 appender 所指定的檔案之中;
還好, console 仍有 service 物件的訊息輸出; 並且提示輸出的 class 名稱為:
service.MyService

於是修改 service 物件中的宣告:
private static final Logger log = LoggerFactory.getLogger('service.MyService')
但, 結果仍不是預期的; 經查明資料後發現, 在 conf/Config.groovy 中 logger 的宣告應該為:
...
  info  dailyAppender: 'grails.app.service.MyService'
...
同時, 要變更 service 物件中的宣告為:
private static final Logger log = LoggerFactory.getLogger('grails.app.service.MyService')
而此時測試的結果, 才真正輸出至預期的檔案之中。

ps.
原來, 我被輸出的訊息 "service.MyService" 給誆了

2010年3月4日

Spring UrlPathHelper 物件的第一次接觸

一般而言, 在 Java web-app 中要取得 browser HTTP 的 request URI [或 query string], 第一個念頭就是使用 Servlet API:

    HttpServletRequest 介面的 getRequestURI() [或 getQueryString()] method

索性, Grails framework 在 Groovy MOP 的支援下, 使得使用這些標準 API(getter) 是輕鬆、直觀的:

    request.requestURI [或 request.queryString]

而且同 JSP 的 EL 一樣, 在 GSP 中可以像 EL 一樣來取值:

    ${request.requestURI} [或 ${request.queryString}]

然而, 該取用 request.requestURI ? 還是取用 request.uri 或者是 request.request.requestURI 才正確呢?
一般的理解, 相較於 servlet API 來說, 直覺會使用 request.requestURI。但, 在 Jetty(開發環境) 與 WebLogic 10.2(執行環境) 兩者 web container 中, 實作上所回應的結果卻不一致, 真出乎我意料之外。

此時, 可以請出 UrlPathHelper class 來解決這樣子的情形:
def helper = new org.springframework.web.util.UrlPathHelper()

def reqURI = helper.getOriginatingRequestUri(request)
def qryString = helper.getOriginatingQueryString(request)

因此, 為避免使用 groovy/Java 與 web container 上實際結果的差異, 經由不斷測試、驗證而得到結果。
軟體開發的解決之道, 仍是老話一句: 測試再測試。這也就是為什麼 TDD(Test-Driven Development) 那麼重要。

2010年2月16日

GORM 物件於 update method 後立即顯示資料

在學習 Grails 的過程中, 剛開始會比較專注於 scaffold 的樣板, 並從中去了解 GORM 物件的操作。
不知有沒有人發現, 在 update action 中的處理:
{
    ...
    if (!someInstacne.hasErrors() && someInstance.save()) {
        ...
        redirect(action: show, someInstance.id)
    }
    ...
}
往往在 show view 所查看到的 GORM 物件並非是剛剛 update 後的資料; 所以解決的方法是:
變更 save method 的參數: flush 為 true; 即
if (!someInstacne.hasErrors() && someInstance.save(flush: true)) {
        ...
但是, 每每在一個 GORM 物件進行 generate-all 後, 得再加以修改該行參數, 顯然不是一個經濟的做法。
於是進行預設 templates 的修訂來達到此一目的是合乎情理的。方法如下:

1) 先安裝 plug-in
grails install-templates
2) project 下, 找出預設 controller 的樣板
vi src/templates/scaffolding/Controller.groovy
3) 修改...
if (! ${propertyName}.hasErrors() && ${propertyName}.save()) {
        ...
    變更為...
if (! ${propertyName}.hasErrors() && ${propertyName}.save(flush: true)) {
        ...
4) 存檔完成

如此, 每當產生預設的 controller 之後, 都會含有此一作用; 省去為每個 controller 要加註 flush 參數。

不過 v1.3.x 版已加了上述的處理了。

2010年2月10日

Grails 中的 MOP 試驗

在臺灣Grails開發者論壇的技術分享討論區中, 提到使用一組特定欄位名稱做為查詢條件(查詢條件+分頁+排序)
引發了我試著做個測試的念頭: 可否直接使用輸入欄位進行 GORM 物件的 criteria 處理 ?

測試的程式片斷:
static YOURS_PROPERTIES = Yours.metaClass.properties

//使用 criteria builder
Yours.withCriteria {
  //取 cri_ 開頭的輸入欄位
  params*.key.grep(~/^cri_.*/).each{ p ->

    //取相對應於 GORM 物件的欄位名稱
    def field = ((String)p).split('_')[1]
    //取出輸入欄位的內容
    def value = params.getAt(p)
    //含有數值? (此為假設; 不過, 並非所有 model 都是如此設計)
    def isLong = value==~ /^[1-9][0-9]*/

    YOURS_PROPERTIES.each{ col->
      if (col.name == field) {
        //若 type 為物件
        if (col.type.name == 'java.lang.Object') {
          "$col.name" {
            eq('id', isLong ? Long.parseLong(value) : value)
          }
        } else {
          eq(col.name, col.type.getDeclaredConstructor(String.class).newInstance(value))
        }
      }
    }
  }
}
使用上述的方式, 僅限於簡單的資料查詢; 因為一般查詢作業不會如此簡化。
不過, 這樣子的試驗對於 MOP 的了解會有多一點的認識。

2010年2月9日

GSP 中使用 Hibernate Criteria Builder

使用 Grails 的經驗中, 處理 GORM 物件的查詢通常會使用 .withCriteria {} 來取得資料, 即便是用在 GSP 中。

可是我的經驗中, 在開發 GSP 程式使用 criteria builder 所得的結果是正常的; 但以 grails-war 來產生 WAR file, 並發佈至 WebLogic App server 後, 卻無法執行。

經研究後的變更方法是:
def yourList =
  new grails.orm.HibernateCriteriaBuilder(
        Yours.class, applicationContext.sessionFactory).list {
    ...
  }
其中第二個參數的 session factory, 它由 applicationContext 這個 bean object 所有, 所以可以直接取得。