概要
namespaceという機能を使えば、Grailsで同名のコントローラを使い分けることが出来るようになります。
別の言い方をすれば、コントローラをpackageに分けて整理することが出来るようになります。
例えば、一般ユーザがアクセスするコントローラはfrontendパッケージ内に配置して、管理画面用のコントローラはbackendパッケージに配置して整理するといった使い方が想定されます。
以前、namespaceについて2回記事をQiitaに投稿しました。
[Grails]namespaceを使って同名コントローラを使い分ける
[Grails]namespaceの使い方
うまく動かないな〜と言いながら上記の記事を書きましたが、結局自分の理解が間違っていただけでした。
今回のものが最終版という位置付けです。
前提条件
アプリケーション名はnamespacetestとしました。
今回は、admin
とuser
とういパッケージを作成し、そのパッケージにそれぞれProfile
というコントローラを作成してみます。
以下のコマンドでコントローラを作成しました。
grails> create-controller user.Profile
| Created file grails-app/controllers/user/ProfileController.groovy
| Created file grails-app/views/profile
| Created file test/unit/user/ProfileControllerSpec.groovy
grails> create-controller admin.Profile
| Created file grails-app/controllers/admin/ProfileController.groovy
| Created file grails-app/views/profile
| Created file test/unit/admin/ProfileControllerSpec.groovy
grails>
実際のコード
特に複雑なものではないので、いきなりソースを見てしまったほうが理解が早いです。
adminネームスペース
コントローラ
package名はcreate-controllerの時点で指定したものが自動で付与されています。
単純にstatic namespace = "admin"
を追加するのみ。
あとは通常通りコントローラを記述します。
package admin
class ProfileController {
static namespace = "admin"
def index() {
}
def redirectFromAdminToUser() {
redirect(namespace:'user', controller: "profile", action: "index")
}
def formPrinter(){
render "This is Admin namespace : ${params.fromUser}"
}
}
ビュー
注意: create-controller
コマンドではディレクトリ(namespace名)までは生成してくれないので、自分でディレクトリをnamespace名で作成する。
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<title></title>
</head>
<body>
<h1>This is Administrator profile</h1>
<g:link namespace="user" controller="profile">Go To User Profile</g:link><br />
<g:link namespace="admin" controller="profile" action="redirectFromAdminToUser">Go To User Profile By Redirect</g:link><br />
<g:form namespace="user" controller="profile" action="formPrinter" >
<g:hiddenField name="fromAdmin" value="Data is from Admin" />
<g:submitButton name="a" value="Sbumit To User" />
</g:form>
</body>
</html>
userネームスペース
コントローラ
package名はcreate-controllerの時点で指定したものが自動で付与されています。
単純にstatic namespace = "user"
を追加するのみ。
あとは通常通りコントローラを記述します。
package user
class ProfileController {
static namespace = "user"
def index() {
}
def redirectFromUserToAdmin() {
redirect(namespace:"admin", controller: "profile", action: "index")
}
def formPrinter(){
render "This is User namespace : ${params.fromAdmin}"
}
}
ビュー
注意: create-controller
コマンドではディレクトリ(namespace名)までは生成してくれないので、自分でディレクトリをnamespace名で作成する。
<%@ page contentType="text/html;charset=UTF-8" %>
<html>
<head>
<title></title>
</head>
<body>
<h1>This is User Profile</h1>
<g:link namespace="admin" controller="profile">Go To Administrator Profile</g:link><br />
<g:link namespace="user" controller="profile" action="redirectFromUserToAdmin" >Go To Administrator Profile By Redirect</g:link><br />
<g:form namespace="admin" controller="profile" action="formPrinter">
<g:hiddenField name="fromUser" value="Data is from User" />
<g:submitButton name="a" value="Submit To Admin" />
</g:form>
</body>
</html>
URLマッピング
元のUrlMappings.groovyに、以下の/*追記スタート*/
から/*追記終了*/
の間を追加する。
class UrlMappings {
static mappings = {
/*追記スタート*/
"/profile/$action?/$id?(.$format)?" {
controller = 'profile'
namespace = 'user'
}
"/admin/profile/$action?/$id?(.$format)?" {
controller = 'profile'
namespace = 'admin'
}
/*追記終了*/
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
}
}
Grails自体の再起動と確認
UrlMappings等を含む、Grailsの設定ファイル系を修正した場合は、何も考えずに一旦Grailsアプリの終了させて、インタラクティブモードからも抜ける。
再度Grailsアプリケーションを立ち上げなおして、http://localhost:8080/namespacetest/profile/indexにアクセスすれば、同じProfileControllerというコントローラがちゃんとnamespace毎に分けられて動作することが確認できます。
おまけ -Scaffolding-
大体のサンプルはこの段階で終わりますが、Grails大好きな自分としてはやはりGORMをScaffoldで扱う場合はどうなるの?というのが気になります。
結論から言うと問題ありません。
例えば、ドメイン用のscaffoldは、全てscaffold
パッケージ内のコントローラで行いたいと想定します。
まず以下のGrailsコマンドでドメインとscaffold用のコントローラを生成します。
grails> create-domain-class Profile
| Created file grails-app/domain/namespacetest/Profile.groovy
| Created file test/unit/namespacetest/ProfileSpec.groovy
grails>
grails> create-controller scaffold.Profile
| Created file grails-app/controllers/scaffold/ProfileController.groovy
| Created file grails-app/views/profile
| Created file test/unit/scaffold/ProfileControllerSpec.groovy
Profileというドメインとscaffoldというネームスペースで動作するProfileControllerを作成しました。
ProfileControllerは以下のように記述します。
package scaffold
class ProfileController {
static namespace = 'scaffold'
static scaffold = namespacetest.Profile
}
そしてUrlMappings.groovyにも同様に情報を追記します。(/*追記スタート*/
から/*追記終了*/
の間)
class UrlMappings {
static mappings = {
"/profile/$action?/$id?(.$format)?" {
controller = 'profile'
namespace = 'user'
}
"/admin/profile/$action?/$id?(.$format)?" {
controller = 'profile'
namespace = 'admin'
}
/*追記スタート*/
"/scaffold/$controller/$action?/$id?(.$format)?" {
namespace = 'scaffold'
}
/*追記終了*/
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
}
}
これで終了!と言いたいところなのですが、Scaffoldが生成するリンク(編集、登録、削除など)が、デフォルトだとコントローラ名とドメイン名が同一のものかつnamespaceは考慮していない、という状態になっています。
今の段階では、URLを手動で指定すればScaffoldの各画面はすでにちゃんと動作する状態ですが、その画面に表示されている編集、登録、削除などのリンクが正常に機能していない状態です。
そこで、Scaffoldが自動で生成するHTMLのテンプレートを修正してあげます。
テンプレートの修正の方法は[Grails]ドメイン名と異なるコントローラでscaffoldingを参照してください。
ざっくり内容を説明すると、
-
grails install-templates
でテンプレートを修正できる状態にする。 - 各GSPの
g:link
とg:form
にnamespaceを追加する。 - Controller.groovyのsave()とupdate()のredirectにnamespaceを追加する。
という流れになります。
具体的には、[Grails]ドメイン名と異なるコントローラでscaffoldingの修正箇所が、
<g:link class="list" action="index" namespace="scaffold"...省略
<g:form url="[action:'save', namespace:'scaffold']" method="POST" ...省略
redirect action: 'show', id:${propertyName}.id, namespace:'scaffold'
といった感じになります。
これで、http://localhost:8080/namespacetest/scaffold/profile/indexにアクセスすれば、ちゃんとnamespaceで管理された状態でscaffoldが利用できます。
所感
何度かチャレンジしてうまく動かない箇所があるな〜と思っていたnamespaceの動作ですが、基本的に自分の書き方(特にUrlMappings.groovy)が悪かったのが原因でした。
今回、aタグでもformタグでもnamespace属性を付与すればちゃんと動作することが確認できました。
Scaffoldもnamespaceをつけてちゃんと動作させることが出来るようになったので、とりあえずnamespaceに関する自分の中の疑問点が消せました。
ドキュメントやユーザベースのナレッジがGrailsの進化に追い付いていないな、という感じで今回もかなり苦労しましたが、噛めば噛むほど味の出るGrails、今後もどんどん調べたことをまとめていきます。
参考
http://www.mscharhag.com/2014/05/grails-controller-namespaces.html
scaffoldに関する追記 (2015/01/21)
renderEditor.templateの中のrenderOneToMany
というメソッドの中にもg:link
があるので、その部分にもnamespace
を追加してあげたほうが良いです。
private renderOneToMany(domainClass, property) {
def sw = new StringWriter()
def pw = new PrintWriter(sw)
pw.println()
pw.println '<ul class="one-to-many">'
pw.println "<g:each in=\"\${${domainInstance}?.${property.name}?}\" var=\"${property.name[0]}\">"
pw.println " <li><g:link namespace=\"scaffold\" controller=\"${property.referencedDomainClass.propertyName}\" action=\"show\" id=\"\${${property.name[0]}.id}\">\${${property.name[0]}?.encodeAsHTML()}</g:link></li>"
pw.println '</g:each>'
pw.println '<li class="add">'
pw.println "<g:link namespace=\"scaffold\" controller=\"${property.referencedDomainClass.propertyName}\" action=\"create\" params=\"['${domainClass.propertyName}.id': ${domainInstance}?.id]\">\${message(code: 'default.add.label', args: [message(code: '${property.referencedDomainClass.propertyName}.label', default: '${property.referencedDomainClass.shortName}')])}</g:link>"
pw.println '</li>'
pw.println '</ul>'
return sw.toString()
}
scaffoldに関する追記 (2015/01/23)
当然ページャー使っているところにもnamespaceを指定する必要が有りました。。。
<g:paginate total="\${${propertyName}Count ?: 0}" namespace="scaffold"/>