レベル: 初級 Scott Davis, Founder, ThirstyHead.com
2009年 3月 10日 連載「Grails をマスターする」の今回の記事では、Grails が Web ページに対して生成する標準の URI (Uniform Resource Identifier) をカスタマイズする方法を Scott Davis が紹介します。URI の主キーを記述的なタイトルに変更することによって、ユーザーが探しているリソースへのパスを、もっと覚えやすく意味のあるパスにすることができます。
前回の「Grails アプリケーションの見栄えを良くする」では、CSS (Cascading Style Sheets) を使って Grails アプリケーション (Blogito ブログ・サイト) の外観を変更する方法を説明しました。今回は、あらゆる Web アプリケーションに欠かせないもの、つまりナビゲートするために使用する URI をカスタマイズする方法を説明します。Blogito のような Web ログにとって、URI は特に重要です。個々のエントリーに戻るためのパーマリンクは、いわゆる名刺のようにインターネットで受け渡されるため、リンクが記述的でわかりやすいものであればあるほど、効果的なリンクになります。
この記事では、より記述的な URI を生成するために、個別に指定した URI をサポートするようにコントローラー・コードをカスタマイズします。続いて UrlMappings.groovy ファイルを微調整して新しい URI へのマッピングを作成し、最後にカスタム URI をより簡単に生成できるようにするカスタム・コーデックを作成します。
URI について
URI の「U」は、正式には「Uniform (一様であること)」を表しますが、「Unique (一意であること)」を表すと言うこともできます (「参考文献」を参照)。例えば http://www.ibm.com/developerworks という URI によって、皆さんが今まさにアクセスしているこの Web サイトが一意に識別されなかったとしたら、URI はほとんど使い物にならなかったことでしょう。また URI は、このリソースに対する覚えやすい ID になるというメリットもあります。この Web サイトには http://129.42.56.216 と入力してアクセスすることもできますが、ドットで区切られた 10 進表記の IP アドレスを記憶するのを厭わないという人はほとんどいないはずです。
上記で説明したとおり、少なくとも URI は一意でなければなりません。さらに、覚えやすい URI であれば理想的です (覚えやすい URI に反対する見方については、囲み記事の「曖昧な URI をめぐる議論」を参照してください)。Grails は間違いなく最初に挙げた要件 (少なくとも URI は一意でなければならないという要件) を満たします。Grails ではコントローラー名、クロージャー名、そしてデータベース・レコードの主キーの組み合わせを使って URI を一意にしています。例えば、ユーザーがデータベースの最初の Entry を見たい場合には、ブラウザーに http://localhost:9090/blogito/entry/show/1 と入力します。
 |
この連載について
Grails は、Spring や Hibernate などのよく知られた Java 技術に「Convention over Configuration (設定より規約)」といった現代のプラクティスを盛り込んだ最新の Web 開発フレームワークです。Groovy で作成された Grails は既存の Java コードをシームレスに統合するだけでなく、スクリプト言語ならではの柔軟性と動的機能を与えてくれます。Grails を学んだら、Web 開発に対する今までの見方がまったく違ってくるはずです。
|
|
主キーを URI に含めるのはデフォルトとしては妥当ですが、この方法は私の繊細な美的感覚には 2 つの点でそぐいません。まず、この URI が実装の内部に多少なりとも入り込んでいることが理由の 1 つで、この付随的なデータベース成果物は、Web サイトの内部を見せてしまっています。一方、Google、Amazon、eBay ではいずれもサービスの背後でデータベースを使用していますが、URI にはその形跡がほとんど表れていません。主キーを URI から排除する 2 つ目の理由は URI に意味を持たせることに関連します。例えば Jane Smith のブログの閲覧者は、彼女を 12 として識別するよりは、jsmith として識別するはずです。それと同じように、ブログ・エントリーを主キーではなくタイトルでリストするほうが、覚えやすい URI という要件を満たします。
User クラスの作成
Blogito はエントリーをサポートする状態になっていますが、ユーザーに関してはまだサポートしていません。そこで、まずは新規 User クラスを作成する必要があります。
 |
曖昧な URI をめぐる議論
URI が一意にリソースを識別しなければならないことには誰もが同意しています。しかし、人間が理解できるようにすることによって URI にメタデータが追加されるという点については、盛んな議論が続いています (「参考文献」を参照)。一部の人々は、URI を一意で記述的なものにして URI に過大な役割を押しつけるのは危険だと主張しています。記述的な URI は長くなり過ぎたり、脆弱になったりするだけでなく、ベースとして使われている技術にリソースの ID を不必要に結合させてしまうことになるというのが、彼らの主張です。
そのような主張はもっともですが、私は謹んで URI がわかりにくくてもよいという前提には反対します。人間が理解できる URI は、よりユーザー・フレンドリーであるだけでなく、これに伴うプラス面は、潜在的なあらゆるマイナス面に勝ると思うからです。明確な URI は覚えやすく、問題に直面した場合にデバッグするにも簡単です。さらに、見てすぐにわかるような決まりに従った URI であれば、その Web サイトがどのような内容かを理解しやすくなり、簡単に探せるようになります。
Grails ではわかりやすい URI への取り組みを、オブジェクト名とコントローラー・メソッドを URI に公開するところから始めます。この記事ではこの取り組みから論理的な結論が得られるように、主キーをテキストからなるわかりやすい ID に置き換えます。ただし、私がこの問題に対する賛成論と反対論両方の言い分を理解している証として言っておきますが、記述的な URI ではなく簡潔な URI が必要なときに tinyurl.com (「参考文献」を参照) などの Web サイトを使用することを心から支持しています。
|
|
まず、コマンド・プロンプトで grails create-domain-class User と入力してください。次に、リスト 1 のコードを grails-app/domain/User.groovy に追加します。
リスト 1. User クラス
class User {
static constraints = {
login(unique:true)
password(password:true)
name()
}
static hasMany = [entries:Entry]
String login
String password
String name
String toString(){
name
}
}
|
login と password は、説明しなくてもわかるように、認証のためのフィールドです。name フィールドは表示目的のフィールドで、例えば、jsmith のログインには「Jane Smith」と表示されます。また、見てのとおり User と Entry の間には 1 対多の関係があります。
1 対多の関係を完成させるため、リスト 2 に記載する static belongsTo フィールドを grails-app/domain/Entry.groovy に追加します。
リスト 2. Entry クラスに追加する 1 対多の関係
class Entry {
static belongsTo = [author:User]
//snip
}
|
フィールド名は、関係を定義する際に簡単に変更できることに注意してください。これで、User クラスには entries という名前のフィールドが設定され、Entry クラスには author という名前のフィールドが設定されました。
通常はこの時点で、対応する UserController を作成して User を管理するための完全な UI を指定するところですが、この作業に取り組む気にはまだなりません。ここではプレースホルダーとしてのスタブ化した User がいくつか必要なだけなので、ユーザーの認証と許可については、次回の連載「Grails をマスターする」で具体的に説明することにします。それまでの間、「どうにか切り抜ける」という精神で、grails-app/conf/BootStrap.groovy を使って新規ユーザーをいくつか追加してください (リスト 3 を参照)。
リスト 3. BootStrap.groovy での User のスタブ化
import grails.util.GrailsUtil
class BootStrap {
def init = { servletContext ->
switch(GrailsUtil.environment){
case "development":
def jdoe = new User(login:"jdoe", password:"password", name:"John Doe")
def e1 = new Entry(title:"Grails 1.1 beta is out",
summary:"Check out the new features")
def e2 = new Entry(title:"Just Released - Groovy 1.6 beta 2",
summary:"It is looking good.")
jdoe.addToEntries(e1)
jdoe.addToEntries(e2)
jdoe.save()
def jsmith = new User(login:"jsmith", password:"wordpass", name:"Jane Smith")
def e3 = new Entry(title:"Codecs in Grails", summary:"See Mastering Grails")
def e4 = new Entry(title:"Testing with Groovy", summary:"See Practically Groovy")
jsmith.addToEntries(e3)
jsmith.addToEntries(e4)
jsmith.save()
break
case "production":
break
}
}
def destroy = {
}
}
|
エントリーがどのように User に割り当てられているかに注目してください。主キーや外部キーをいじくり回す必要はありません。GORM (Grails Object Relational Mapping) API のおかげで、リレーショナル・データベースの理論ではなく、オブジェクトの観点で考えることができます。
続いて、前回の記事で作成した部分テンプレート、grails-app/views/entry/_entry.gsp に多少の調整を加えて、Entry.lastUpdated フィールドの隣に作成者が表示されるようにします (リスト 4 を参照)。
リスト 4. _entry.gsp への作成者の追加
<div class="entry">
<span class="entry-date">
<g:longDate>${entryInstance.lastUpdated}</g:longDate> : ${entryInstance.author}
</span>
<h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}
</g:link></h2>
<p>${entryInstance.summary}</p>
</div>
|
${entryInstance.author} は、User クラスで toString() メソッドを呼び出します。あるいは、${entryInstance.author.name} を使用して、明示的に選択するフィールドを表示することも可能です。この構文を使えば、ネストされたクラス階層を任意のレベルまでトラバースすることができます。
これまでの変更がどのように反映されているかを見てみましょう。grails run-app と入力して、Web ブラウザーで http://localhost:9090/blogito にアクセスしてください。画面は図 1 のようになっているはずです。
図 1. 新しく追加された作成者が表示されたエントリー
Blogito が複数のユーザーをサポートするようになったので、次は、サイトの閲覧者が作成者別のエントリーを表示できるようにします。
作成者別エントリーの表示
この記事での最終的な目標は、http://localhost:9090/blogito/entry/list/jdoe のような URI をサポートすることです。この URI では、主キーの代わりに User.login が使われていることに注目してください。最終目標を達成するまでの過程では、ページネーションについても多少調整する必要があります。
scaffold された EntryController.list の振る舞いでは、User を基準にフィルタリングすることはできません。リスト 5 に、list クロージャーのデフォルト実装を記載します。
リスト 5. デフォルトの list 実装
def list = {
if(!params.max) params.max = 10
[ entryInstanceList: Entry.list( params ) ]
}
|
この実装は、パスの最後に追加されるオプション・ユーザー名をサポートするように拡張しなければなりません。そこで、grails-app/controllers/EntryController.groovy を編集して、リスト 6 に記載する新しい list クロージャーを追加します。
リスト 6. 作成者によるリストの制限
class EntryController {
def scaffold = Entry
def list = {
if(!params.max) params.max = 10
flash.id = params.id
if(!params.id) params.id = "No User Supplied"
def entryList
def entryCount
def author = User.findByLogin(params.id)
if(author){
def query = { eq('author', author) }
entryList = Entry.createCriteria().list(params, query)
entryCount = Entry.createCriteria().count(query)
}else{
entryList = Entry.list( params )
entryCount = Entry.count()
}
[ entryInstanceList:entryList, entryCount:entryCount ]
}
}
|
まず注目してもらいたいのは、params.max と params.id がエンド・ユーザーによって指定されない場合には、賢明なデフォルト値が設定されるという点です。flash.id については、とりあえず無視してください。これについては後で、ページネーションの問題を話題にするときに説明します。
params.id の値は、通常は整数です。正確に言えば、これが主キーとなります。/entry/show/1 や entry/edit/2 などといった URI は見慣れていることでしょう。grails-app/conf/UrlMappings.groovy にマッピングを設定して、params.name や params.login のような、より記述的な名前を返すという方法もとれますが、現行のマッピングはすでに、アクション名に続くパス要素を取得して params.id に保管するようになっています。そこで、ここでは単純に既存の振る舞いの利点を生かすことにします。リスト 7 に記載する URLMapper.groovy で、params.id を返すデフォルト・マッピングを確認してください。
リスト 7. UrlMappings.groovy のデフォルト・マッピング
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?"{
constraints {}
}
//snip
}
}
|
これは User の主キーではないため、いつものように User.get(params.id) を使用することはできません。代わりに、User.findByLogin(params.id) を使用する必要があります。
一致する User が見つかった場合には、クエリー・ブロックを作成します。これは、Hibernate Criteria Builde (「参考文献」を参照) の動作で、この例の場合には、リストを特定の作成者と一致するエントリーに絞り込んでいます。この場合も GORM のおかげで、主キーや外部キーではなく、オブジェクトの観点で考えることができます。
params.id と一致する作成者がいない場合には、全エントリーのリストが返されます。つまり、entryList = Entry.list( params ) となります。
entryCount の値は、明示的に計算していることに注意してください。scaffold された GSP (GroovyServer Pages) コードは通常、<g:paginate> タグに含まれる Entry.count() を呼び出します。しかしこの場合にはフィルタリングされたリストが返される可能性があるため、コントローラー内の変数で値を計算しなければならないというわけです。
params.id の値を flash.id に保管することで、アプリケーションはクエリー基準を <g:paginate> タグに渡せるようになります。grails-app/views/entry/list.gsp で <g:paginate> を調整し、新しい entryCount 変数と、flash のスコープに保管されたパラメーターを利用できるようにしてください (リスト 8 を参照)。
リスト 8. カスタム・ページネーションに合わせた list.gsp ページの調整
<div class="paginateButtons">
<g:paginate total="${entryCount}" params="${flash}"/>
</div>
|
Grails を再起動して、Web ブラウザーで http://localhost:9090/blogito/entry/list/jsmith にアクセスしてください。すると、図 2 のような画面が表示されます。
図 2. 作成者別のエントリーのリスト
ページネーションが引き続き機能することを確認するため、http://localhost:9090/blogito/entry/list/jsmith?max=1 と入力してください。Previous ボタンと Next ボタンをクリックすれば、Jane のブログ・エントリーだけが表示されることを確認できます (図 3 を参照)。
図 3. カスタム・ページネーションのテスト
作成者を基準とした基本的なフィルタリングが用意できたので、次はもう一歩踏み込んで、さらにわかりやすいカスタム URI を作成します。
カスタム URI の作成
UrlMappings.groovy ファイルは、新しい URI を作成する上で並外れた柔軟性をもたらしてくれます。http://localhost:9090/blogito/entry/list/jsmith でも確かに機能しますが、ユーザーからの最新リクエストで、http://localhost:9090/blogito/blog/jsmith といった URI もサポートしなければならなくなったとします。このような場合も、まったく問題ありません!リスト 9 に記載する新しいマッピングを UrlMappings.groovy に追加すればよいだけのことです。
リスト 9. UrlMappings.groovy へのカスタム・マッピングの追加
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?"{
constraints {
// apply constraints here
}
}
"/"(controller:"entry")
"/blog/$id"(controller:"entry", action="list")
"500"(view:'/error')
}
}
|
これで、/blog で始まる URI は controller エントリーと list アクションにリダイレクトされることになります。$user や $login のほうが記述的ですが、$id を Grails の規約に合わせておくと、"/$controller/$action?/$id?" と "/blog/$id"(controller:"entry", action="list") の両方で同じエンドポイントを指すことができます。
Web ブラウザーに http://localhost:9090/blogito/blog/jsmith と入力して、このマッピングが機能することを確認してください。
User での作業はこれで終わったので、次は、Entry にもわかりやすい URI を作成するという作業に専念します。
カスタム・コーデックの作成
User.id の代わりに User.login を使用すると、URI にスペースが含まれなくなるため、URI は簡単になります。確かに、現在はこの「スペースなし」のルールを強制するための検証ルールはありませんが、「スペースなし」のルールへの準拠を確実にするために検証を追加することは簡単です (「参考文献」を参照)。
一方、URI の Entry.id を Entry.title に置き換える場合についてはどうでしょう。タイトルにはほぼ間違いなく、スペースが含まれることになります。このスペースが含まれるという問題を解決する 1 つの方法は、Entry クラスにもう 1 つフィールドを追加して、エンド・ユーザーにスペースなしでタイトルを再入力させることですが、この方法は理想的ではありません。なぜなら、ユーザーの作業が多くなるとともに、開発者にとっても別の検証ルールを作成してユーザーが正しく入力することを確実にしなければならなくなるからです。それよりも賢い解決策は、どこで Entry.title が使用されているかに応じて、Grails にスペースを自動的にアンダーバーに変換させることです。そのためには、カスタム・コーデック (コーデックはコーダー・デコーダーの省略形) を作成する必要があります。
grails-app/utils/UnderscoreCodec を作成して、リスト 10 のコードを追加してください。
リスト 10. カスタム・コーデック
class UnderscoreCodec {
static encode = {target->
target.replaceAll(" ", "_")
}
static decode = {target->
target.replaceAll("_", " ")
}
}
|
Grails には、すぐに使えるコーデックがいくつか組み込まれています。具体的には、HtmlCodec、UrlCodec、Base64Codec、JavaScriptCodec です (「参考文献」を参照)。このうち、HtmlCodec が、生成される GSP ファイルに含まれる encodeAsHtml() メソッドと decodeHtml() メソッドのソースとなっています。
独自のコーデックを構成に追加するとしても、組み込みコーデックと使用するのと同じく簡単です。Grails は grails-app/utils ディレクトリーにある任意のクラスに Codec サフィックスを付けて、encodeAs() および decode() メソッドを String に追加します。この例では、Blogito に含まれるすべての String には現在、このようにして encodeAsUnderscore() と decodeUnderscore() という 2 つの新しいメソッドが追加されています。
test/integration 内に、リスト 11 の UnderscoreCodecTests.groovy を作成して、カスタム・コーデックを検証してください。
リスト 11. カスタム・コーデックのテスト
class UnderscoreCodecTests extends GroovyTestCase {
void testEncode() {
String test = "this is a test"
assertEquals "this_is_a_test", test.encodeAsUnderscore()
}
void testDecode() {
String test = "this_is_a_test"
assertEquals "this is a test", test.decodeUnderscore()
}
}
|
コマンド・プロンプトで grails test-app と入力してテストを実行すると、リスト 12 のような結果が表示されるはずです。
Listing 12. Output showing successful a successful test run
$ grails test-app
-------------------------------------------------------
Running 2 Integration Tests...
Running test UnderscoreCodecTests...
testEncode...SUCCESS
testDecode...SUCCESS
Integration Tests Completed in 157ms
-------------------------------------------------------
|
コーデックの動作
UnderscoreCodec が用意できたので、ユーザーとエントリー・タイトルの両方が含まれる URI (http://localhost:9090/blogito/blog/jsmith/this_is_my_latest_entry など) をサポートするために必要なものはすべて揃いました。
初めに UrlMappings.groovy で、オプションの $title をサポートするように /blog マッピングを調整します (リスト 13 を参照)。Groovy では末尾の疑問符は、対象要素をオプションとして設定することを思い出してください。
リスト 13. URI マッピングでのオプション・タイトルの許可
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?"{
constraints {
// apply constraints here
}
}
"/"(controller:"entry")
"/blog/$id/$title?"(controller:"entry", action="list")
"/entry/$action?/$id?/$title?"(controller:"entry")
"500"(view:'/error')
}
}
|
続いて EntryController.list を調整して、新しい params.title の値を考慮するようにします (リスト 14 を参照)。
リスト 14. コントローラーでの params.title の処理
class EntryController {
def scaffold = Entry
def list = {
if(!params.max) params.max = 10
flash.id = params.id
if(!params.id) params.id = "No User Supplied"
flash.title = params.title
if(!params.title) params.title = ""
def author = User.findByLogin(params.id)
def entryList
def entryCount
if(author){
def query = {
and{
eq('author', author)
like("title", params.title.decodeUnderscore() + '%')
}
}
entryList = Entry.createCriteria().list(params, query)
entryCount = Entry.createCriteria().count(query)
}else{
entryList = Entry.list( params )
entryCount = Entry.count()
}
[ entryInstanceList:entryList, entryCount:entryCount ]
}
}
|
クエリーでは like を使用して、より柔軟な URI にしました。例えば、ユーザーが /blog/jsmith/mastering_grails と入力すると、mastering_grails で始まるすべてのタイトルが返されます。より厳密にしたい場合には、代わりに eq メソッドをクエリーで使用すれば、完全一致を要求することができます。
Web ブラウザーで http://localhost:9090/blogito/jsmith/Codecs_in_Grails と入力して、新しいコーデックの動作を確認してください。画面は図 4 のような表示になるはずです。
図 4. ユーザー名とタイトルによるブログ・エントリーの表示
まとめ
URI は、Web アプリケーションには不可欠です。Grails の実用にかなったデフォルトは、作業を開始するときに使うにはうってつけの方法ですが、開発者としては、Web サイトの要件に最適な URI をカスタマイズすることもお手の物でなければなりません。これまでの作業に励んだ甲斐あって、Blogito には User と Entry が用意できました。しかしそれよりも重要な点は、皆さんが URI で主キー以外のものを使用して目的のサイトを表示できるようになったことです。この記事で説明したように、コントローラー・コードを調整し、マッピングを UrlMappings.groovy に追加し、そしてカスタム・コーデックを作成することで、よりわかりやすい URI を作成することができます。
次回は、Blogito の User を認証できるようにログイン・フォームを作成します。ユーザーがログインした後は、HTML、イメージ、さらには MP3 ファイルなどをブログ・エントリーの本文用ファイルとしてアップロードできるようになります。それでは、次回の記事まで Grails を楽しみながらマスターしてください。
参考文献 学ぶために
製品や技術を入手するために
- Grails: Grails の最新リリースをダウンロードしてください。
- Blogito: Blogito アプリケーションの完成版をダウンロードできます。
議論するために
著者について
記事の評価
|