2012年10月13日 星期六

如何在Grails 上實現多對多的關係(四) : 怎麼實作

寫了這麼多廢話,該說如何實作了。我們先從使用者開始。


class LoginUsers{
   static hasMany = [roleLink : RoleUsers]
   String userid
   static constraints = {
      userid(unique:true)  
   }

 List roles(){
    return roleLink.collect{it.role}
 }
 List addRole(Roles r){
    RoleUsers.link( this , r )
    return roles()
 }
 List addRoles(List roleids){
    for(rid in roleids){
       RoleUsers.link( this , Roles.get(rid))
    }
    return roles()
 }
 List removeRole(Roles r){
    RoleUsers.unlink(this,r)
    return roles()
 }
 void removeAllRoles(){
    RoleUsers.unlinkAllRolesFromUser(this)
 }
}
稍微解釋一下。還記得測試的程式嗎, aska.roles() 就可以得到角色,背後怎麼做呢?使用者的類別中我們這樣定義過…

   static hasMany = [roleLink : RoleUsers]

不用宣告一個 Set roleLink 在上面,GORM 自動幫我們加入了。所以呼叫 aska.roleLink() 就會馬上得到屬於aska的List<RoleUsers>

但我說過 RoleUsers 不算是一個真正的「物件」,我們並不關心它( 雖然它要做很多事),我們要的是群組。所以這一部份,我們要再加上collect後,封裝在 roles() 的方法裡丟出Roles。 

如果你還不知道collect的話,請看前面的介紹,這裡很簡潔的把 RoleUsers裡屬於 aska 的群組蒐集起來丟出去。

class LoginUsers {
    static hasMany = [roleLink : RoleUsers]
}
class RoleUsers {
    LoginUsers user
    Roles role
    static constraints = {  
    }
    static belongsTo = [user:LoginUsers , role:Roles]
}

這樣夠清楚吧。有這樣的宣告後,aska只要呼叫 roleLink.collect{it.role} 一行就完事了,非常給力。

可想而知 Roles的部份也是 userLink.collect{it.user} 來得到一個群組下的使用者們。這邊我就不再解釋一次了。

aska.addRole ,因為我們要新增一個 RoleUsers的物件,所以責任上我們就給RoleUsers去做。先定義方法叫做link。加多數的群組,就是呼叫link 多次。removeRole就先定義叫 unlink。 Roles的類別,可想而知也定義了類似的方法,不再解釋

class Roles {
        String name
        static constraints = {
             name(unique:true)
        }
        static hasMany = [ userLink:RoleUsers  ]
 List users(){
    return userLink.collect{it.user}
 }
 
 List addUser(LoginUsers u){
    RoleUsers.link( u , this)
    return users()  
 }
 List addUsers(List uids){
    for(uid in uids){
       RoleUsers.link(LoginUsers.get(uid) , this)
    }
    return users() 
 } 
 List removeUser(LoginUsers u){
    RoleUsers.unlink( u , this)
    return users()
 }
 void removeAllUsers(){
    RoleUsers.unlinkAllUsersFromRole(this)
 }
}

重頭戲在 RoleUsers 裡。

class RoleUsers {
 LoginUsers user
 Roles role
 static constraints = {
  
 }
 static belongsTo = [user:LoginUsers , role:Roles]
 
 static RoleUsers link(LoginUsers u , Roles r){
   def lk = RoleUsers.findByUserAndRole(u,r)
   if(!lk){
   //if not in db then we start to link
     lk = new RoleUsers()
     u.addToRoleLink(lk)
     r.addToUserLink(lk)
     lk.save(flush:true)   
   }
   return lk
 }
 static void  unlink(LoginUsers u , Roles r ){
   def lk = RoleUsers.findByUserAndRole(u,r)
   if(lk){
     u.removeFromRoleLink(lk)
     r.removeFromUserLink(lk)
     lk.delete(flush:true)
  }
 }
 static void unlinkAllRolesFromUser(LoginUsers u){
  def lks = RoleUsers.findAllByUser(u);
  if(lks){
   for(lk in lks){
      lk.user.removeFromRoleLink(lk)
      lk.role.removeFromUserLink(lk)
      lk.delete(flush:true)
   }
  }
 }
 static void unlinkAllUsersFromRole(Roles r){
  def lks = RoleUsers.findAllByRole(r);
  if(lks){
    for(lk in lks){
      lk.role.removeFromUserLink(lk)
      lk.user.removeFromRoleLink(lk)
      lk.delete(flush:true)
   }
  }    
 }
}

我假設你已經會基本的GORM了,所以只說重點。

在做鏈結與非鏈結時,記得先「解除關係」。

link 這個方法中透過


  • LoginUsers 的 hasMany 裡定義的變數名稱為roleLink,
  • 並且RoleUsers屬於 LoginUsers
  • 於是Grails就會在LoginUsers中,自動幫你合成方法 addToRoleLink



addToRoleLink 如此就變成是內建的方法了,不用再實作了。

記得要從「一」方去加「多」方,因為有主權的一方擁有責任。
aska.addToRoleLink(link),把自己加到鏈結當中。Roles也要做一樣的事。最後再save 那個 link (也就是RoleUsers) 則大功告成。

相反的,非鏈結要做的步驟也是一樣。把二邊的關聯去除後,再把 link 刪除。若你忘了先把關聯解除,就會得到exception。

再來是一個使用者想要把所有的群組刪掉。首先得到屬於他的 List<RoleUsers>後,一樣是要先解除關聯。要從「一」方來呼叫removeFrom,所以還得要先連回「一」方再分別解除,這也就是 lk.role.remove... 與 lk.user.remove....的意義。

程式是否少到令你難以置信?這中間沒有任何的 xml 任何的 annotation。類別的關聯性也不用再做文件說明,非常的明白易懂。定義好 RoleUsers裡的方法實作後,再來就沒有什麼事可以做了。呃,對,你也不用再管DB裡的表格了,Grails會自動幫你處理。

快速的做一些ui 的假想

有一個使用者的列表頁,點進去某個使用者之後,會出現基本資料維護,下方是他可以使用的群組。

在controller 裡我們快速的呼叫這二個方法塞成params丟到gsp中馬上就可以使用。


[role_list : Roles.list() , checked_role_list : aska.roles() ]

在維護頁裡,按下確認後回到controller,我們會先刪除aska的所有群組,然後再加有勾選的部份。

aska.removeAllRoles()
aska.addRoles(params.checkedRoleId)
二行搞定… 若前端頁面變成奇怪的新式UI,使用者點一下某個群組就會動態的Ajax往後端呼叫做更新,再按一下就刪除這個群組,那也是 aska.addRole(Roles.get(rid)) 與 aska.removeRole(Roles.get(rid)) 而已… 花了蠻多時間寫的,希望對大家有幫助。

如何在Grails 上實現多對多的關係(三) : 三者的結構

首先你一定會先定義類別吧,我們一次要定義三個類別出來。

class LoginUsers {

}
class Roles{

}
class RoleUsers{
      //這個就是多對多的中間表,我們需要它來做很多事。
}
再來我們開始定義它們的關聯,也是很直觀。


// LoingUsers 1 <-> * RoleUsers * <-> 1 Roles

class LoginUsers {
    static hasMany = [ roleLink : RoleUsers ]
}
class Roles{
    static hasMany = [ userLink : RoleUsers ]
}
class RoleUsers{
    LoginUsers user
    Roles role
    static constraints = {
  
    }
    static belongsTo = [user:LoginUsers , role:Roles]
}
LoingUsers 1 <-> * RoleUsers * <-> 1 Roles 別說你看不懂喔,正規化是幼幼班的課程。
這邊比較特別的是…明明LoginUsers與Roles參照同一個表格,為何我取不同的名字,一個是roleLink一個是userLink ? 這是你使用上的自由…

從使用者的觀點來看,這是一個群組鏈結表。
從群組的觀點來看,這是一個使用者鏈結表。

所以我訂不同的名字。為了方便,你也可以都叫做 link:RoleUsers,這邊就隨你高興囉! 不過 Roles 不一定只和Users 做多對多,他也可以與功能做多對多,所以取名時還是多加幾個字比較好。

中間表裡的每一個record,每一個物件,都一定對到1個使用者與1個群組。

從使用者的觀點來說,他是一對多的,就像一個人可以寫很多本書,多方的書「屬於」作者。 所以這裡的多方 RoleUsers 是「屬於」使用者的。

從群組的觀點來說,也是一對多的,一個群組裡可以有多個使用者。

所以在這個中間鏈結表中,他同時「屬於」二者,中間表沒有什麼主權,所以他「屬於」別人。

用這樣的方式去理解應該就簡單了。

剛剛說,中間表裡的每一列都對應到一個使用者與一個群組,所以你當然要宣告他們才連的回去。 結構的部份搞清楚了,後面就不是很難理解了。內容請看下一章。

如何在 Grails 上透過 GORM 實現多對多的關係。(二) 我就是要這樣呼叫

二個類別,LoginUsers 和 Roles,還記得吧。

        LoginUsers aska = new LoginUsers(userid:'aska')
     LoginUsers gina = new LoginUsers(userid:'gina')
     Roles admin_role = new Roles(name:'admin')
     Roles normal_role = new Roles(name:'normal')
     aska.save(flush:true)
     gina.save(flush:true)
     admin_role.save(flush:true)
     normal_role.save(flush:true)
aska 是admin , gina 是一般使用者。簡單來說這個測試就是如此。 我們要做的測試是,從aska,gina的角度加入群組,以及從群組的角度加入使用者。 當然也要從aska與gina的角度來退出群組,以及從群組的角度來刪除使用者。 GORM 是「絕對直觀」派的東西。你先看我下面怎麼寫,就知道「絕對直觀」是指什麼。 我們先來測試從群組的觀點來加入使用者,並且倒過來從使用者的角度來得到群組。

        println 'add aska to admin_role' + admin_role.addUser(aska)
     println 'add gina to normal role '+ normal_role.addUser(gina)
     println 'add aska to normal role' + normal_role.addUser(aska)
     println 'aska should have two roles = '+aska.roles()
     println 'gina should have one role = ' + gina.roles()
     println 'admin user should be aska= ' + admin_role.users()
     println 'normal users should be aska and gina= '+normal_role.users()
我們實作的程式,就是這樣呼叫。也就是說,這樣寫一定是你最滿意的。得到一個群組的物件時,呼叫 addUser 就可以加入user,還有比這樣寫更簡單的方式嗎? 先說一下,這邊的程式一定都不會過,因為這些方法都還不存在。你可能有想到說,多對多不是有「中間」的表格嗎,怎麼都沒看到? 我們在實作的時候,會透過一個中間類別來做處理,這個中間類別對我來說,不應該出現在controller或是service 之中,它不是我們關心的物件。雖然等下他要做很多事! 再來我們測remove

        //test remove 
     normal_role.removeUser(gina)
     println 'remove gina from normal users, now normal role should be only aska = '+normal_role.users()
     normal_role.removeAllUsers()
     println 'remove all users from normal role, now normal role = '+normal_role.users()
     println 'aska should have no groups after unlink all roles' + aska.removeAllRoles()
好,當我們在刪除的時候,要是你是從使用者的角度出發,你只想呼叫removeRole(role) , removeAllRoles()這樣吧。我所謂的「絕對直觀」,就是程式員的本能,如果可以這樣寫就下班有多好,從這個角度來設計就對了。從群組的角度來說,就是 removeUser(user) , removeAllUsers()這樣,反之亦然,找不到更短的寫法了。 在得到使用者的群組亦是 aska.roles() , 得到管理員群組下的人也該是admins.users(),這就是直觀導向的寫法。 下面再從使用者的角度,加入一堆群組。這個在維護畫面中常常見到啊,管理者勾一勾這個人能加入的群組,將群組id送到controller等待寫入db,所以我們也是需要這樣的功能。

        aska.addRoles([1,2])
     println 'add aska to admin and normal group, now aska join : ' +aska.roles()
     println 'add gina to normal role' + gina.addRole(normal_role)
     println 'remove gian from both roles'+gina.removeAllRoles()
     println 'gina should have no roles now'+gina.roles()
     println 'add gina to admin and normal roles '+ gina.addRoles([1,2])
     println 'clear all user from role'
     normal_role.removeAllUsers();
     admin_role.removeAllUsers();
     println 'try to add people to roles.'
     println 'add aska and gina to normal role.'+normal_role.addUsers([1,2])
     println 'add aska to admin role.'+admin_role.addUsers([1])
     println 'test remove user from roles'
     aska.removeAllRoles()
     gina.removeAllRoles()
     println aska.roles()
     println gina.roles()

我不用解釋你也看的懂,這就是我們要的程式,再來下一章要講背後我們要如何實作,以得到這直觀寫法。

如何在 Grails 上透過 GORM 實現多對多的關係。(一) 測試先行

當我完成 GORM 多對多的測試時,真的覺得很酷。坦白說,我是SQL 派的人,物件導向的Query Language 背後其實沒啥效率,不管是新增或是刪除常常多做了幾筆SQL。在複雜的程式中,其實直接透過SQL 來達成任務,在效能上是比較能接受的。

不過反過來思考,多對多的維護功能其實很常見。如果一直在程式中SQL 來SQL 去的,「程式的可讀性」會大大的降低。Hibernate出現後,程式員透過物件的方式來操作資料庫,反倒成了顯學。這邊的爭論可以寫在另外一篇文章了。

這顯學蔚然成風,但我畢竟是散淡的人,多寫幾個字總覺得麻煩,但是又無處可去。

Grails 的做法非常的另人驚艷。
不講理論,先講結果,當多對多完成的時候,我可以怎樣去得到我要的東西?
首先我們的  domain class 為 LoginUsers 和 Roles。
這個很單純,就是一個使用者可以加入多個群組,而一個群組裡也可以包含多個使用者。
Grails 講的是結果。所以你想怎麼做,可以先寫在「整合測試」中。
寫在整合測試,在跑的時候,就有db連線的功能。

整合測試

我以前一直不太懂,先寫測試有什麼好處。在傳統 Java 開發中已經夠慢了,想到要寫測試就頭大。但後來終於理解「測試先行」的意思了。

那就是先決定「我就是要這樣呼叫」。
而後再把程式的肉慢慢做出來。

這樣的差別在於,你若是「呼叫」的人,一定是想寫最少的程式來得到你要的結果。而這樣的導向,會讓程式易於理解。若先寫類別與方法,寫完了有時候發現外部程式呼叫起來很不順,這也是因為沒有從別人的觀點來開始的原因。

不過我們不是要講整合測試

不囉嗦,開始寫我們想要的呼叫方式。請看下一篇。




2012年10月9日 星期二

在Grails 中如何寄 email

首先,先在Command mode輸入

 grails install-plugin mail
一般應該是用公司的mail server發送吧 ? 第二步你要設定email 的相關資訊。打開

/your_project/grails-app/conf/Config.groovy
加入 

grails{
 mail {
      host = "78.78.78.78" 
      port = 25     
    }
}
grails.mail.default.from="itdep@aska.com" 

再來要怎麼做呢… 在 傳統的 Java 來說,你要 import 一堆 Java Email 相關的的類別,中間的辛酸就不用再提了。在Grails 的話,在程式任何一處只要寫這樣就可以寄出了 !

sendMail {     
          to "aska@aska.com.tw"     
          subject "test , post created."     
          body "${params.content}" 
       }
透過動態語言的好處,每一個類別都「突然會」 sendMail 了!
也就是「動態的」他們都擁有了這個方法。
grails email plugin 的好處在於,你不需要特別再寫一個工具類別,或是繼承,或是任何方法來達成寄信的功能。
讓程式從無到有學會寄信,不到一分鐘就可以完成。
更多的細節,請參考 http://grails.org/plugin/mail

2012年10月7日 星期日

Grails 如何取得 hibernate 的 session ?

有的時候還是忍不住想要下native sql ,在 grails 中要如何取得  session 呢?
答案簡單到不行…

首先在controller 宣告一個  def sessionFactory

在你要執行的方法中,寫

def ss = sessionFactory.getCurrentSession()

這個 ss 就是hibernate 的session ! 等下,那怎麼取得 sessionFactory ? 別忘了Grails 內建Spring的注入功能,該注的他都幫你注入進去了。


def ret = ss.createSQLQuery("select * from post").list()
println ret.get(0)[0]

這種方法回傳的就是 List<Object[]> !
Grails 幫我們做掉太多事了! 邊學邊拍桌子啊 !

2012年10月1日 星期一

Grails 的ORM 關聯(一) belongsTo, hasMany

我們直接舉1對1,或是1對多的例子。

一對多來說,一個作者可以發表多篇文章。
那這個關聯我們要從哪邊開始加?我們可以先在「文章」(多)中先加 belongsTo。
文章「屬於」作者。這個應該沒有疑問。

class Post {
  String content
  Date dateCreated
  static constraints = {
   content(blank: false)

  }
  static belongsTo = [ user : User ]
}

最後的 static belongsTo = [ user : User ] 當然是我們注意的重點。
這個 [ user : User ] 其實是一個map,不過一個小寫的user ,一個大寫的 User有何用處?
這個user是給我們用做參考關聯的。透過這個寫法,Post 也可以反著查詢 User了。

例如有一個畫面是閱讀某人寫的文章,最上面需要出現那個人的id。因為我們在這裡已經建立好倒回去關聯 User 時的變數名稱 「user」,所以只要 post.user.userId 便很快的可以得到我們要的結果。

你可能也看過
static belongsTo = User 
的寫法,如果這樣寫的話,post 就不能反過來查到對應的 user了。如果沒有必要從 Post 反查 User 時,這樣寫當然有一點效能上的優勢。

belongsTo 還有要注意的一點,既然都說自己「屬於」別人了,所以當 User 從資料庫上被幹掉的同時,Post 也沒有任何存在的必要了。這個就是「屬於」的觀念。
當然在 User 這個類別上,也要記得加上
static hasMany = [ post : Post ]
如此一來關聯就正式接上了。接上之後有很多預設的神奇招式,在下一章會講到。

Grails 中 Domain class 如何自訂驗證

Groovy 彈性的語法,總是讓人有一點不習慣,但其實抓住幾個原則就行。
先來看這個自訂驗證的範例。

  static constraints = {
     userId (unique:true , size : 3..20)
     password(
       nullable:true , 
       size : 6..8 ,
       validator :{ password , user ->
          return password != user.userId
       }      
     )
     homepage(nullable:true , url : true)
   
  }


  1. 這個例子要達到的目的就是,使用者的密碼不要和帳號一樣即可。最大的限制式本身就是一個閉包。一般的驗證常用的就是 nullable , size 啊等等的。它有一點像 Java 後來加入的動態數量參數。所以你可以想到什麼就臨時加什麼 ( 當然缺點就是編譯時期無法知道是否正確 )。
  2. 我們主要看的是在 password裡的「自訂驗證」,這個時候要加 validator : value。在 value 的部份,我們傳入一個閉包,這邊我用藍色框起來。從key :value -> validator : value -> validator : 閉包。這樣就不會覺得 {   } 的出現很突兀了。
  3. 這個validator 所用的閉包,可以接受二個參數。password 當然就是使用者傳進來的參數囉。第二個參數是user 物件,這是方便你做比較用的。簡單來說,閉包的參數就是二個,一個是password ,一個是user 物件。若你不需要用到user 物件,那只要指名 password -> 即可。
  4. 後面要回傳true or false,所以你要告訴他怎樣是成功,怎樣驗證是失敗。