読者です 読者をやめる 読者になる 読者になる

ビューティフルWebコード

美しいWebサイトのコーディングについて説明をしていきます。

エラー処理(例外処理)を共通化しよう!

エラー処理はとても重要!でも大変なので、ルールを決めよう!

エラー処理(例外処理)は非常に重要な処理です。
真面目に処理しようとしたら、正常系のコードよりもエラー処理のコードの方が多くなっていくと思います。
どのプログラム言語もtry-catchなどの例外処理が言語仕様として組み込まれていると思います。
ですが、それぞれのエンジニアがそれぞれ思い思いにエラー処理を実装していたのでは、とてもいい品質のコードはできません。
効率よく開発するためにエラー処理にもルールを設ける必要があります。

エラーの種類を大別する!

自分は、エラーの種類は以下の2種類があると考えています。

  • 自己復帰できるエラー
  • 自己復帰できないエラー(システムエラー)

これらを以下のように定義します。

自己復帰できるエラーとは

障害ではない。ログレベルで言うならWARN。
ユーザが自分の力で復帰できるのが望ましい。
やり直しをさせることで、処理を完了することができる。

  • URLのパラメータ改ざんによりDB検索をしたがデータが存在しなかった。
  • Cookieが改ざんされた。
  • セッションが切れた。
  • などなど

自己復帰できないエラー(システムエラー)

障害。ログレベルで言うならERROR、FATAL

  • インフラ障害(ネットワーク、DB)
  • バグ

エラー処理の実装を開発者にどう伝えるか

自分がリーダーだったら、こうします。

エラー処理はするな!

え?って思われるかもしれません。
要件や、詳細設計としてフローチャート、シーケンス図レベルで記載されるロジックエラーについては、それぞれのエンジニアに任せましょう。
でも、それ以外のエラー処理に関しては一切エラー処理をするな!と言います。

すべてのエラーを共通エラー処理に集めてしまい、そこでエラー処理を行います。

なぜ、そうなるかを考えていきましょう。

実際にどういうエラーが起こるか検討してみる

/users/1234 (/users?id=1234) というURLについて考えてみます。

以下の処理が行われるものとします。

  • UsersControllerのIndexActionメソッドが呼び出される。
  • DBからid=1234のユーザを検索する。
  • 検索結果をViewに渡して表示する。

この中で、どういうエラーが発生するか、異常系を考えてみます。

  • a. id=1234のユーザが存在しなかった。
  • b. そもそもバグがあって、プロダクトコード内で実行中に例外が発生した。(ぬるぽetc)
  • c. 設定が間違って(DBの接続文字列が変わって)いてDBに接続できなかった。
  • d. ネットワーク障害で、DBに接続できなかった。
  • e. その他、現段階では想定していないのエラーが発生するかもしれない。

ここで考察です。
a〜eのエラーについて、どういうエラー処理をさせたいでしょうか。
要件がある場合は従う必要があります。
要件が提示されなかったから何もしなくていいのかというとそうは行きません。
少なくとも共通エラー画面を用意して、エラーを通知すべきです。

考えられるエラー処理を次に記載します。

a. id=1234のユーザが存在しなかったは、自己復帰できるエラー

ユーザが意図的にURLを改ざんし、存在しないidを入力してきた場合や、
ユーザのURLをブックマークしていたけど、ユーザが退会してしまっていた、といったことも考えられます。

以下のエラー処理が考えられます。(要件に従う必要があります。要件がなければ提案する事象です。)

  • 共通エラー画面に遷移させる。(例外をthrowして共通エラーハンドラに処理を任せる。)
  • 404 NotFound扱いにする。(サイト内の404表示の手続きに従う。)
  • 該当するユーザは存在しないか、退会しています。という独自画面を表示する。(Action内独自処理を入れる)

共通エラー画面に遷移させるのは少し乱暴です。共通エラーは障害時しか表示してはいけないからです。
また、共通エラーハンドラではエラーログを出力するため、ログ監視サービスが稼働してる場合、アラートが上がります。
もし、共通エラーハンドラに任せる場合、これは障害ではないよ!っていう独自の例外の型を準備して、それをthrowし、エラーログを出さないようにする必要があります。

その他は自己復帰できないエラー(システムエラー)

プログラムのバグ、ネットワーク障害、設定ミス(バグ)すべてユーザには関係のないサイト側の問題です。
ユーザはどうすることもできません。
これらは共通エラーページを表示するべきです。個々にエラー処理をしてはいけません。

フレームワークのエラー処理の機能を理解しよう!

どのフレームワークにも必ずエラー処理の機能が準備されています。
また、フレームワークで処理されなかった例外は、Webサーバー(Apache, IIS)などが500エラーを返します。

エラー処理機能を細かく切り出すと以下のようになります。
それぞれでハンドルされなかった場合、次の機能にエラー処理を任せます。

  • Actionメソッド内でのエラー処理(try-catch)
  • Actionに設定したフィルタまたはアノテーションによるエラー処理
  • Controllerによるエラーイベントの処理
  • Controllerに設定したフィルタまたはアノテーションによるエラー処理
  • フレームワークの共通(グローバル)エラー処理
  • Webサーバーによる500処理

理想論を言えば、

  • そのエラーがURL固有(Action固有)であればAction内で処理すべきです。
  • そのエラーがController固有であればControllerのエラーイベント処理で処理すべきです。
  • そのエラーがサイト共通であれば、フレームワークの共通エラー処理に任せるべきです。
  • Webサーバーの500処理は起きてはいけませんが、フレームワークが処理仕切れない例外が発生することもあるので、共通エラー画面に表示するhtmlをサーバーの500にも設定しておくべきです。404も同様にサーバーにも設定しておきましょう。

これでも、結構複雑ですね(笑)。これをいろんなエンジニアの独断に任せたらまた大変なことになりそうですね。

つまり、要件にあるURL固有のエラー処理以外は、

エラー処理はするな!

となるのです。
共通エラー処理についてはリーダーや、メインプログラマ、1人に任せておきましょう(笑)

共通エラー処理としてやっておくといいこと

エラー画面が表示されると、エンドユーザは不安になります。
緊急であればサイト運営に問い合わせなどを行うことになるでしょう。
保守運用を依頼されている場合、障害対応となります。
障害対応はなるべく時間をかけずに簡潔に行えるようにしておくと、運用コストを削減できるでしょう。
(保守費用が月額で決まっている場合、時間がかかればかかるほど、エンジニアのボランティア作業になってしまう)

自分がよく実装するのは以下のような仕組みです。

  • GUIDなどで一意のお問い合わせID(エラーID)を生成します。
  • 例外オブジェクトの内容、メッセージをお問い合わせIDと一緒にエラーログに出します。
  • エラー画面を表示するのですが、ここでお問い合わせIDを一緒に出力します。メーラーを起動する仕組みであればお問い合わせIDをタイトルなどに含めてあげるといいでしょう。

ここで注意なのですが、
エラー処理では、エラーを起こす可能性のある仕組みを実装してはいけません。
たとえば、お問い合わせIDを生成した場合、その内容をDBに記録する!なんて実装をしてはいけません。
DB障害でエラーハンドラーに飛んできた場合、エラーハンドラでDB書き込めないでしょ?

また、ZABIXなどの監視ツールを使いエラーログを監視するといいですね!

実際にエラーが起こったらどうするの?

共通エラーハンドラで出力したエラーログを確認します。
それが、コードのバグなのか、インフラエラーなのかを切り分けます。
コードのバグであればバグ対応してください。今後そのエラーはなくなりサイトの品質が上がります。
インフラのバグであれば、インフラ対応をしてください。冗長化構成をするなど、障害に強いインフラを目指してください。

画面のURLのHTTPステータスコードを考察しよう!

ステータスコードを整理しよう!

200 OK

通常のView表示です。正常に処理が行われました。


400 BadRequest

URLのQueryStringが異常な場合などに利用したりします。

QueryStringの異常の場合、Not Foundを使っても違和感がないと思うので、使うかどうかは検討してください。


401 Unauthorized

Basic認証などが設定されている場合にWebサーバーが返します。

Webサイトの実装として、認証ができなかった場合にこのステータスを返す設計をすることもあります。


403 Forbidden

アクセス権限がない場合にWebサーバーが返します。

サイト側の実装としては、ユーザのランクや権限に応じてアクセスできない場合、このステータスを返す設計をすることもあります。


404 NotFound

よく知られたステータスコードです。

静的なリソースファイルが存在しない場合、Webサーバーが返します。

Controller、Actionへのルーティングができなかったときに、フレームワークが返します。

パラメータをURLに含めるURL設計の場合、対象のパラメータのデータが存在しなかったときに、プログラムにて自前で返す場合もあります。


405 Method Not Allowed

GETしか受け付けないURLでPOSTでアクセスがあった、など、ルーティングできない場合に、フレームワークによっては、フレームワークが勝手に返します。

400 BadRequestが利用されることもあります。


500 InternalServerError

プログラムで例外が発生し、フレームワークで処理されなかった場合、Webサーバーが返します。

共通エラーハンドラーでプログラムで処理した場合でも、エラー画面を表示するときにはこのステータスで返します。


503 ServiceUnavailable

メンテナンスモードです。
フレームワークで対応するのかWebサーバーで対応するのか、要検討してください。


考察

QueryStringなどの入力値異常をBad RequestとするのかNot Foundとするのか、方針を統一しておくといいと思います。

ステータスコードのエラー画面を準備しよう!

サイトとして考えられるHTTPステータスコードを検討しておくと、とても幸せになります。

Webのプログラムの処理が入る前に、Gatewayや、Webサーバーがステータスコードを返す場合もあります。GatewayやWebサーバーがエラーを返す場合、各ステータスに応じた静的なHTMLを配置しておくと良いと思います。

Apacheが返すエラー画面が表示されてしまうと、サーバーにApacheを利用していることがばれてしまい、セキュリティ、ユーザビリティの観点であまりよろしくありません。

URL設計 URLを管理するクラスを作ろう!

仕様変更が悪いのではない。仕様変更に絶えられない実装が悪いのだ!

プロジェクトは仕様変更との戦いです。仕様が変わったせいで、全てのコードを見直し、テストを全てやりなおすなんて日常茶飯事です。仕様が変わった事を恨んでも仕方がないので、仕様が変わってもすぐに対応できる準備をあらかじめしておく必要が有ります。

Web開発でよくあるのが、HTMLやControllerのソースコードにURLが直接記述されているので、URL変更が思うようにできない、という悲鳴です。

じゃあ直接記述しなければいいじゃん!

URLを管理するクラスを作りましょう!

URLを管理するクラスを作って、URLの処理を閉じ込めます。
パラメータ無しのURLであれば public const で公開すれば良いでしょう。
パラメータ有りの場合は、メソッドにしてパラメータを引数で渡せるようにしましょう。
パラメータが複雑であれば、パラメータのクラスを作ってそのクラスを引数で渡すようにしましょう。
URLエンコーディングや、暗号化されたパラメータなど、パラメータ生成ロジックが発生するのであれば、ロジックもメソッドに閉じ込めましょう。

//URL管理クラス
public class Urls 
{
        //エラー画面のurl 生成 
        //ErrorCodeはenumや定数にしておき、
        //わざわざエラーコード一覧の資料を見なくても実装できるようにしてあげましょう。
        public static string Error( ErrorCode ecd )
        {
            return string.Format( "/error?ecd={0}", ecd )
        }
        
        //ユーザ画面のURL 
        // idはintなので型指定してあげましょう
        public static string User( int id )
        {
            return string.Format( "/user?id={0}", id )
        }

        //認証用のURL tokenが必須なので、token生成やエンコード処理を閉じ込めている
        public static string Auth()
        {
              var encodedToken = UrlEncode( AuthToken.CreateToken() );
              return string.Format( "/auth?token={0}", encodedToken );
        }
}
  • エラー画面URLであれば、エラーコードがenumになっており、enumにコメントをつけてあれば、どういうエラーなのかを資料を見なくても実装できます。
  • ユーザ画面URLにいては、型がintのパラメータが必要ということがわかります。
  • 認証URLについては、複雑なロジックが内部に含まれていますが、ロジックが閉じ込められているため、利用者はロジックを意識することなく、呼び出すだけでURL生成ができます。

URL管理クラスアンチパターン

以下のコードはどうでしょう?

//アンチパターン
public class Urls 
{
        //エラー画面のurl フォーマット
        public const  string ErrorFormat = "/error?error_code={0}";
        
        //ユーザのTOP画面のフォーマット
        public const  string UserFormat = "/user?id={0}";

        //認証URLのフォーマット tokenを生成してURLエンコードをしてセットしてください。
        public const string AuthFormat = "/auth?token={0}";
}

これはパラメータ付きURLのフォーマットを返しています。
これではせっかくURL管理クラスを作る意味がほとんどありません。

  • すべてのURLについて利用する側でFormat変換をしなくてはいけません。
  • エラーコードを指定する場合、エラーコードの種類がわかりません。別の資料を見ながら手入力するのでしょうか。
  • userIdに関しては、idはintなのか、stringなのかわかりません。(よく使われるURLなんだから仕様理解しろよ!というツッコミもあるかもしれない)
  • 認証URLに関しては tokenってどうやって生成するのかわからない場合、知ってる人に質問しないといけません。

知ってる人が誰なのか?最悪たらい回しにされます。無駄な時間が過ぎます。
プライドが高い人は、token生成アルゴリズムが記載された資料を見つけて、自前で実装してしまうかもしれません。

  • URLエンコード忘れてしまった場合、テストで認証エラーが起きて原因調査に時間が取られます。

すべてに言えますが、URL組み立てロジックがいたるところに分散してしまい、無駄なソースコードが増えます。
stringのformatは便利ですが、パラメータ数が一致しないときには例外が発生するなど、色々面倒になります。
ControllerやViewでformatを組み立てると、そのコードが実行されないと、バグがわかりません。

正しいURL管理クラス設計を!

たったひとつ、URLを管理するクラスを正しく設計し、共有するだけで、URLに関する無駄な結合テスト単体テストレベルで可能になります。
URL組み立てロジックをそれぞれ実装しなくて済みます。
複雑なURL組み立てをそれぞれの開発者が意識しなくてよくなります。

URLを管理するクラスが肥大化してきたよ!

よくあります。

classを階層化するなり、Bridgeパターンを適用するなり、細分化してください。
クラスのパスが変わってもビルドエラー箇所を直せば済みます。
結合テストを書いてるのであれば、結合テストを実行してあげてください。それだけで済みます。

まとめ

  • URL管理クラスを作り、パラメータなどのロジックはこのクラスに閉じ込める。stringフォーマットをpublicで公開しない。
  • URL管理クラスの単体テストをすれば、URL生成ロジックのテストは完了する。
  • 肥大化してきたら、細分化して、メンテナンスしやすいようにする。

URL設計 画面のURLの命名規則を決めよう Action名はパターン化しよう!

Action名をパターン化する!

入力新規->確認->完了 の画面遷移がたくさんあるサイトだったとします。CMSとかですかね。

  • 登録は /●●●/input -> /●●●/confirm -> /●●●/completed

といったルールを決めておくとよいです。

統一されていると嬉しいこと

  • ソースが読みやすい コントローラを開いたときに、入力新規は input、確認はconfirm、完了はcompleteといったルールが決まっているとすぐに該当のソースを確認できる。
  • 機能追加要件でのURL設計が楽。「ああ入力フォーム系か。Controller名さえ決めればいいなあ。Actionは決まってるからコピペ。」
  • 機能追加の実装が楽。「とりあえず、別のコードをパクって参照するmodelのクラスを実装すれば動くぜ!」

実際に画面の種類を洗い出す!

画面の要件定義から画面一覧をエクセルなどでまとめてみると、大抵、同じような画面が多い事に気づきます。

よくある種類の画面をピックアップしてみます。

コンテンツ表示
  • TOP画面系(いろいろ詰め込まれている、menu的な意味になったり、yahooのようなポータルになったり、facebookのような個人ページだったりします)
  • 一覧画面(記事やコンテンツの一覧を表示する画面です。)
  • 詳細画面(記事やコンテンツの1つを表示します。おすすめ記事や、同じカテゴリのトップ記事など一覧の機能が付加されていることもあります)
  • 単体の画面(このサイトについて、利用規約、会社概要、紹介画面、有料会員訴求など)
form入力
  • 入力新規画面(質問や、日記などを投稿します。パスワード変更など設定系も含まれます。)
  • 確認画面(入力内容を確認する画面です。確認画面を挟まないこともあります。)
  • 完了画面(完了したことを表示する画面です。)
  • 入力編集画面(設定内容を変更するなどの画面です。編集後、確認画面、完了画面と画面遷移することもあります)
その他
  • 認証関連(ログイン、ログアウト)
  • エラーページ(404 Not Found, 500 Internal Server Error など)

JavascriptMVCを使ったシングルページ実装などもあるので、投稿機能自体がTOPや一覧に含まれることもあります。

具体例

たとえばスポーツニュースサイトの画面構成を考えてみましょう。

  • サイトTOP画面(プロ野球結果速報、Jリーグ結果速報、相撲、、、、、、、)
  • 各スポーツ別TOP画面(プロ野球TOP、JリーグTOP、、、)
  • 各記事一覧(試合結果一覧、各ニュース記事一覧)
  • 詳細画面
  • お問い合わせフォーム

といった具合ですね。

サンプルでAction名を提示します!

  • TOP画面系 index
  • 一覧画面 index, list, etc...
  • 詳細画面 article, item, detail, etc...
  • 単体の画面 各々適当な名前をつけてください。
  • 入力新規画面 input,create, entry, new, insert, post, index(Action名省略という意味)
  • 確認画面 confirm,conf,check, review, etc...
  • 完了画面 finished, completed, end, thanks, etc...
  • 入力編集画面 input?id=xxxx(新規はid無し), edit, update, put, etc....
  • 認証関連 login, logout, auth, etc
  • エラーページ 404, 500 (これはアクセスしたURLでリライトするのか、リダイレクトさせるのか方針による)

Action名を決める場合、たとえば入力新規はHTTPMethodを意識してpostにしたら、編集はputにするとか、
CRUDを意識して create,update にするとか、ポリシーに従ったルールを決めると良いと思います。

また、URL一覧をexcelで管理し、controller名、actiom名を別セルで入力し、URLを自動生成させるようにしておくなどの工夫をすると、命名規則を管理しやすく、イレギュラー対応などが即時にできるようになります。

URL設計 画面のURLの命名規則を決めよう Action名は動詞!?

Action名は動詞!

Controllerが主語、Actionが動詞であればそのURLは
/Controller名 / Action名 は S(主語)V(動詞)の関係になります。

オブジェクト指向プログラミングでいうところのクラス名、メソッド名も主語、動詞の関係ですよね。

実際に、MVCフレームワークではController名はクラス名、Action名はメソッド名になるので、やはり主語、動詞の関係であるべきです。

Indexは動詞!?

よく既定のAction名(省略可能なAction名)としてIndexが割り当てられているMVCフレームワークは多いと思います。
Indexって動詞の意味もあるけど目次って意味だと名詞だよねぇ。。例外なの?

自分は例外だと思っています(笑)

RestfulAPIのURL設計に毒されるな

最近RestfulAPIの設計が市民権を得てきて、ナウでヤングな設計になってきています。
SOAPや、XML-RPCのような古いAPIの形式の立場が無くなってきていてちょっと寂しい。

RestfulAPIの思想である、
動詞はHTTP MethodでURLはリソースを表す!
を崇拝するあまり、画面のURLに動詞を使うことに抵抗を持つ若いエンジニアが結構います。

今、話題にしているのは、画面のURL設計です。APIのURL設計ではありません!

WebAPIのURL設計と画面のURLは似て非なるものです!!!

CRUD(Create,Read,Update, Delete)を表す場合、
RestfulAPIではHTTP Methodの(POST, GET, PUT, DELETE)を使いますが、ブラウザからのアクセスではPOSTとGETしか使えません。

GETな動作

  • ブックマーク
  • 検索エンジンからの遷移
  • URLバーに直接URL入力!
  • aタグによるリンク
  • metaタグによるリダイレクト
  • javascriptによるlocation.hrefによるリダイレクト
  • formでmethod="GET"

POSTな動作

  • formからmethod="post"

まあ、ほとんどGETなんですよね。つまりHTTPメソッドを活用できない以上、URLで代用するしかないのですね。

どんどんAction名に動詞を使いましょう。

URL設計 画面のURLの命名規則を決めよう Controller名は名詞!で、単数形?複数形?

MVCフレームワークを使ったURL設計についてです。

Controller名は主語!

Controllerが主語、Actionが動詞であればそのURLは
/Controller名 / Action名 は S(主語)V(動詞)の関係になります。

オブジェクト指向プログラミングでいうところのクラス名、メソッド名も主語、動詞の関係ですよね。

実際に、MVCフレームワークではController名はクラス名、Action名はメソッド名になるので、やはり主語、動詞の関係であるべきです。

Action名については別記事で考察します。

Controller名は主語なんだから名詞!

はい、名詞を使ってください。

  • /beatiful(形容詞)/web
  • /directly(副詞)/access
  • /go(動詞)/home

おかしいですよね。

Controller名は単数形なの?複数形なの?

名詞の集合は単数形か複数形か、DB設計でも議論になります。
同じように、URL設計でもよく議論になります。

今の流行に乗るならば複数形がいいと思います。

理由としては下の二つのURLを考えてみてください。

  • /users
  • /users/1234 (/users?id=1234)

/usersの方はaction名ユーザ一覧を表すとします。
/users/1234はユーザ一覧にて、ユーザIDが指定されたと考えることができます。

DBのテーブルに置き換えてみてください。
usersはテーブルです。つまり集合です。

  • /users は select * from users; を表します。
  • /users/1234 (/users?id=1234) は select * from users where id=1234 を表します。

※DBのテーブルの複数形についてはORMとの相性などもあり、単数形が好まれることもあります。

結局、単数形、複数形どっちなの

Webサイトとしての要件を満たせるのか満たせないのか、という観点でいうと、どっちでもいいです。

DBのテーブル名のルールと統一すると分かりやすくなるでしょう。DBが複数形ならURLも複数形!

命名規則なんて宗教論争です。
宗教論争は無駄です。(絶対に結論が出ない)
無駄な宗教論争を回避するためにルールを決めるのです!

開発あるある(勝手にルールを変える)

立ち上げの初代エンジニアは単数形で設計しました。規約にも単数形にしてます。と明記していました。
それを引き継いだ2代目エンジニアが、
「なんで単数形なの?普通複数形だろ。。。。」
とぼやき始めます。単数形の規約を削除しました。
複数形2代目が追加したURLはすべて複数形になってしまいました。
それを引き継いだ3代目エンジニア
「なんで統一されてないんじゃーーー!!!」

                                                          • -

ルールを決めたら守りましょう。

URL設計 パラメータにQueryStringを使うか、URLに含めるのか

設計パターン

user情報を表示する画面のURL設計をする場合以下の二通りに分けられるのではないでしょうか

  1. /user?id=12345(QueryString)
  2. /user/12345(URLに含める)

どちらが優れてるのか?と言われると、どちらが優れてることはないと思います。

以下のことができれば、どちらでもいいですよね。

  • きちんとControllerのActionメソッドにルーティングされること
  • パラメータを取得できること
  • パラメータのバリデーションを共通ルールで行えること

あとは、設計者の好みの問題も大きいと思います。

大事なのは、きちんと利用するMVCフレームワークについて、上記を可能にする手段が提供されているかを確認する必要が有ります。

  • QueryStringの場合

どのフレームワークでも対応可能と思います。

  • URLに含める場合

きちんとルーティングができるかを確認しておく必要が有ります。調査に時間がかかったり、要件を満たせなくなることは避けたほうがいいです。

QueryStringを使うべきURL QueryStringを使うべきではないURL

階層構造をもつ場合はQueryStringを使わないURL

WindowsエクスプローラMacだとFinder)のディレクトリ階層みたいな感じですね。
例1 日付

  • /schedule/2010 2010年のスケジュール一覧
  • /schedule/2010/01 2010年1月のスケジュール一覧
  • /schedule/2010/01/15 2010年1月15日のスケジュール一覧

これをqueryStringで表すと

/schedule?y=2010&m=01&d=15

QueryStringは順番を入れ替えたり、パラメータを含める、含めないを可変に行えるので、次のようなアクセスもできてしまいます。

/schedule?d=15

dがあるときはy,mは必須
mがあるときはyは必須
という複雑なバリデーションを求められます。

パラメータの項目が可変になる場合はQueryStringを使ったURL

主に検索などです。
ユーザに利用してもらう検索については、テキストボックスが一つしかないgoolgeのような検索が使い易いですが、業務システムやCMS(コンテンツ管理システム)のように社内で利用されるシステムの検索などは検索条件を複雑に指定できるように、複数の inputを配置することが多いです。この場合はやはり、QueryStringになります。

例 在庫検索(品名、品番、カテゴリ、、、)

  • /stock/search?pnm=xxx&pcd=xxx&pca=xxxx

検索条件の場合、一般的な仕様として

  • パラメータを含めれば検索条件に含める
  • パラメータを含めなければ検索条件に含まれない

という組み合わせが存在します。パラメータをURLに含める形式の場合これらは実現できません。

また、フレームワークによってはパラメータを一つのModelObjectとしてActionメソッドに渡すことができ、Model内部でバリデーションを行い、Actionに渡ったときにはバリデーション結果が分かっているという機能を持つものも多いです。
複雑な入力が行われる画面についてはQueryStringをお勧めします。