2
2

More than 5 years have passed since last update.

[Grails]namespaceの使い方(最終版)

Last updated at Posted at 2014-11-11

概要

namespaceという機能を使えば、Grailsで同名のコントローラを使い分けることが出来るようになります。
別の言い方をすれば、コントローラをpackageに分けて整理することが出来るようになります。
例えば、一般ユーザがアクセスするコントローラはfrontendパッケージ内に配置して、管理画面用のコントローラはbackendパッケージに配置して整理するといった使い方が想定されます。

以前、namespaceについて2回記事をQiitaに投稿しました。
[Grails]namespaceを使って同名コントローラを使い分ける
[Grails]namespaceの使い方

うまく動かないな〜と言いながら上記の記事を書きましたが、結局自分の理解が間違っていただけでした。
今回のものが最終版という位置付けです。

前提条件

アプリケーション名はnamespacetestとしました。
今回は、adminuserとういパッケージを作成し、そのパッケージにそれぞれ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"を追加するのみ。
あとは通常通りコントローラを記述します。

namespacetest/grails-app/controllers/admin/ProfileController.groovy
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名で作成する。

namespacetest/grails-app/views/admin/profile/index.gsp
<%@ 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"を追加するのみ。
あとは通常通りコントローラを記述します。

namespacetest/grails-app/controllers/user/ProfileController.groovy
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名で作成する。

namespacetest/grails-app/views/user/profile/index.gsp
<%@ 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に、以下の/*追記スタート*/から/*追記終了*/の間を追加する。

namespacetest/grails-app/conf/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は以下のように記述します。

namespacetest/grails-app/controllers/scaffold/ProfileController.groovy
package scaffold

class ProfileController {

    static namespace = 'scaffold'
    static scaffold = namespacetest.Profile

}

そしてUrlMappings.groovyにも同様に情報を追記します。(/*追記スタート*/から/*追記終了*/の間)

namespacetest/grails-app/conf/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を参照してください。
ざっくり内容を説明すると、

  1. grails install-templatesでテンプレートを修正できる状態にする。
  2. 各GSPのg:linkg:formにnamespaceを追加する。
  3. 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を追加してあげたほうが良いです。

src/templates/scaffolding/renderEditor.template
    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を指定する必要が有りました。。。

src/templates/scaffolding/index.gsp
<g:paginate total="\${${propertyName}Count ?: 0}" namespace="scaffold"/>
2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2