e-learning、オラクル研修、LMS(学習管理システム)のiStudy

e-learning、オラクル研修、LMS(学習管理システム)のiStudy

第123回「実用WEBアプリ 文書管理システム(4)文書表示」

2015.04.02

こんにちは。インストラクターの蓑島です。

4月になりました。これから穏やかで過ごしやすい季節になりますね。

前回は、文書を登録するプロシージャを作成しました。今回はその文書を表示するプロシージャを作成しましょう。これで最低限の機能(登録→表示)が揃います。しかし、文書の更新機能なども当然必要ですから、それは後の回で説明する予定です。さらに文書検索、ディレクトリを意識した階層表示など、今後、機能を順次、追加したいと考えています。

プロシージャの解説の前に、いつものように文書やディレクトリの情報を格納するDOCS表の定義を掲載しておきます。表の定義を参照しながら、プロシージャのロジックを確認してください。

SQL> SHOW USER
ユーザーは"SCOTT"です。

1
2
3
4
5
6
7
8
9
10
11
SQL> DESC DOCS
  名前                                      NULL ?    型
  ----------------------------------------- -------- ----------------------------
  ID                                        NOT NULL NUMBER           --番号(主キー)
  TITLE                                     NOT NULL VARCHAR2(300)    --タイトル
  KBN                                       NOT NULL NUMBER           --区分1:文書 2:ディレクトリ
  HONBUN                                             CLOB             --本文(文書のときのみセット)
  OYA_DIR                                            NUMBER           --親ディレクトリ
  SEQ                                                NUMBER           --ディレクトリ内の順序
  INSERT_DATE                               NOT NULL DATE             --登録日
  UPDATE_DATE                               NOT NULL DATE             --更新日

今回作成する、文書を表示するプロシージャの名前は、DOC_SHOW にします。パラメータとして、文書の番号(DOCS表のID列)を指定します。つまり、指定された番号の文章を表示するということです。そしてその番号が、ディレクトリの番号である場合は、エラーにするのではなく、そのディレクトリ以下の文書やサブディレクトリのタイトル一覧を表示するようにしたいと考えています。つまり、指定された番号が文書の場合と、ディレクトリの場合で、処理する内容が違うわけです。とりあえず今回は、指定された番号が文書(KBN=1)の場合の処理を実装します。指定された番号がディレクトリ(KBN=2)の場合の処理は来週、実装します。

以下のようにコーディングしました。
(注意)以下のソースコードはブラウザ表示のために、山括弧(「<」と「>」)を全角にしています。
コピーして実行する方は、必ず、すべての山括弧(「<」と「>」)を半角に変換してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
CREATE OR REPLACE PROCEDURE DOC_SHOW
(P_ID  IN VARCHAR2)
IS
/*****************************************/
/*  文書・ディレクトリ  表示処理         */
/*****************************************/
   V_ID NUMBER;  -- 文書(またはディレクトリ)の番号
   V_ERRMSG   VARCHAR2(1000);   -- エラーメッセージ
   REC         DOCS%ROWTYPE;    -- DOCS表の1行を格納する変数
   USER_ERROR     EXCEPTION;    -- ユーザ定義例外
/************************************************/
/*  文書を表示するローカルプロシージャ          */
/************************************************/
   PROCEDURE  DOC_DISPLAY(REC1 IN DOCS%ROWTYPE)
   IS
   BEGIN
      HTP.P( '<HTML>' );
      -- タイトルをセット
      HTP.P( '<HEAD><TITLE>' || REC1.TITLE || '</TITLE></HEAD>' );
      HTP.P( '<BODY>' );
      HTP.P( '<FONT COLOR="BLUE"><I>' ||REC1.TITLE || '</I></FONT>'
      || '<FONT SIZE="-1">['   --タイトルの横に更新日時をセット
      ||TO_CHAR(REC1.UPDATE_DATE, 'YYYY/MM/DD HH24:MI' ) || ']</FONT>' );
      -- 本文は改行を含むので、PREタグで囲み、そのままを表示する
      HTP.P( '<PRE>' || REC1.HONBUN || '</PRE>' );
      HTP.P( '</BODY>' );
      HTP.P( '</HTML>' );
   END DOC_DISPLAY;
/************************************:***********/
/*  ディレクトリを表示するローカルプロシージャ  */
/************************************************/
   PROCEDURE  DIRECTORY_DISPLAY(REC2 IN DOCS%ROWTYPE)
   IS
   BEGIN
      HTP.P( '該当番号はディレクトリです。<BR>' );
      HTP.P( 'ディレクトリ表示機能は次回、実装予定です。' );
   END DIRECTORY_DISPLAY;
/*****************************************************************/
/*   実行部                                                      */
/*****************************************************************/
BEGIN
   /********************/
   /* チェック         */
   /********************/
   V_ID := TO_NUMBER(P_ID);     --パラメータの文章番号を数字に変換
   -- その文章番号でDOCS表の1レコード(行)を取得
   SELECT * INTO REC  FROM DOCS WHERE ID = V_ID;
   /**************************************************************/
   /* チェックがOKなら、区分に応じて文書またはディレクトリを表示 */
   /**************************************************************/
   IF REC.KBN = 1 THEN     -- その行が文書(KBN=1)なら
      DOC_DISPLAY(REC);    -- 文書表示のローカルプロシージャをコール
   ELSIF  REC.KBN = 2 THEN -- その行がディレクトリ(KBN=2)なら
      DIRECTORY_DISPLAY(REC);  -- ディレクト表示のローカルプロシージャを
   ELSE
      V_ERRMSG := 'KBN列の値が1,2以外です。ありえません。' ;
      RAISE USER_ERROR;
   END IF;
EXCEPTION
   WHEN VALUE_ERROR THEN
        HTP.P( '文書番号が数値ではありません' );
   WHEN NO_DATA_FOUND THEN
        HTP.P( '指定された番号が文書表(DOCS表)に存在しません' );
   WHEN USER_ERROR THEN
        HTP.P(V_ERRMSG);
   WHEN OTHERS THEN
        V_ERRMSG := SQLERRM;
        HTP.P(V_ERRMSG);
END ;
/
 
プロシージャが作成されました。

解説します。

まず、このプロシージャで大きな特徴は、宣言部、つまり3行目の「IS」から、41行目の「BEGIN」までの間に、ローカルなプロシージャが宣言されていることです。ローカルなプロシージャとは、PL/SQLブロックの宣言部で宣言されたプロシージャであり、そのPL/SQLブロックの中でのみ使用可能です。それらのローカルなプロシージャは、文書を表示するプロシージャ(名前 DOC_DISPLAY)と、ディレクトリを表示するプロシージャ(名前 DIRECTORY_DISPLAY)です。

これらのプロシージャを実行部(41行目の「BEGIN」と59行目の「EXCEPTION」の間)で、コールしているわけです。

このように、宣言部にローカルなプロシージャを宣言し、実行部でそれをコールする形にすることで、実行部そのものの記述は単純となり、処理の全体像を把握しやすくなります。

次に、全体の流れを順に見ていきましょう。
このDOC_SHOWプロシージャ全体のパラメータは、「P_ID」です(2行目)。データ型がVARCHAR2なのは、HTTPプロトコルのリクエストパラメータとして渡されるパラメータだからです。HTTPのリクエストパラメータは常に文字型なので、VARCHAR2です。
尚、PL/SQLプロシージャのパラメータのデータ型にはサイズ指定はしないので、VARCHAR2(n)ではなく、常に、VARCHAR2です。

このパラメータを最初に実行部でチェックします。
つまり45行目でTO_NUMBER関数で数値に変換し、V_ID変数に代入しています。もし、数値に変換できなければ、VALUE_ERROR例外となり、60~61行目のVALUE_ERROR例外ハンドラを実行(「文書番号が数値ではありません」と表示)して、処理が終了します。数値である場合は次のステップとして、47行目のSELECT INTO文を実行して、該当するDOCS表の行をREC変数に代入します。もしSELECT INTO文が0件である場合はNO_DATA_FOUND例外なので、62~63行目のNO_DATA_FOUND例外ハンドラを実行(「指定された番号が文書表(DOCS表)に存在しません」と表示)して処理を終えます。

したがって、51行目以下は確実に該当する行を1行取得できた場合の処理です。
ここで、その行が文書(REC.KBN=1)なら、その行レコード(REC)をDOC_DISPLAYプロシージャに渡してコールし、文書表示の処理を行います。同様にその行がディレクトリ(REC.KBN=2)なら、DIRECTORY_DISPLAYプロシージャをコールし、ディレクトリ表示の処理を行うわけです。

繰り返しますが、このように実行部の記述が単純となり、処理の全体像が把握しやすいですね。

また、ローカルなプロシージャ、DOC_DISPLAYと、DIRECTORY_DISPLAYではDOCS表のレコード型変数を「REC」ではなく、それぞれ「REC1」、「REC2」という名前で宣言して使用していますが、その意味は、プロシージャ外部の変数(REC 9行目で宣言)を直接参照していないことを強調しているのです。
実は好ましいことではありませんが、ローカルプロシージャはプロシージャ外部の変数を参照することが可能です。したがって、たとえば、14行目で、DOC_DISPLAYプロシージャのREC1パラメータを宣言せずに、17行目から27行目の間にある「REC1」という名前をすべて「REC」にして、52行目でコールするときに、パラメータRECを指定しないとしても、処理は正しく実行できます。
しかしそのような記述はいろいろと問題があります。例えば、他のプログラムから再利用するため、独立させて通常のストアド・プロシージャとして作成(CREATE OR REPLACE PROCEDURE~)しようとしても、コンパイルが失敗するとか、あるいはロジックの影響する範囲を局所化できないので開発効率が悪いなど、問題があります。

そこで、直接外部の変数を参照しなくても済むように、ローカルプロシージャ、DOC_DISPLAYとDIRECTORY_DISPLAYは、レコード変数(REC 9行目)をパラメータとして受け取ります(52行目、54行目)。REC1パラメータ(14行目)と、REC2パラメータ(32行目)の宣言はそのためのものです。このとき、REC1と、REC2を「REC」という9行目で宣言したREC変数と同じ名前で宣言し使用することもできます。その場合、ローカルプロシージャ内のRECと、ローカルプロシージャの外部のRECと、おなじ名前で変数やパラメータが複数存在することになります。しかし変数の有効範囲(スコープ)の原則から、近いものが優先なので、名前は同じでも、あくまで9行目のREC変数とは別物であり問題はありません。
しかし誤解を招きやすいので、別物であることを強調して、「REC1」、「REC2」と名前を変えているわけです。

ここで皆さんは、『「REC」を「REC1」や「REC2」にコピーして渡しているわけだから、メモリに同じデータを複数保持し、メモリを必要以上に消費するのでは?文書レコードは大量の文字データだし、大丈夫?』と思われるかもしれません。
しかしこの心配はありません。なぜかというと、この場合、「REC」を「REC1」や「REC2」に渡すとき、値をコピーで渡しているのではなく、「参照」で渡しているからです。
一般にパラメータを渡すときに値をコピーして渡す方法を「値渡し」、参照で渡す方法を「参照渡し」といいます。PL/SQLでは、どちらの方法が使われるかはその時の状況によって自動的に選択されます。
一般に、モードが「IN」のパラメータ(例 REC1 IN VARCHAR2)では、リモートプロシージャコールでない限りは(例えば東京のデータベースからデータベースリンクにより大阪のデータベースのプロシージャをネットワーク経由でコールするような状況ではないような場合は)、メモリ空間を共有できるので、参照渡しになります。今回はローカルプロシージャでモードINですから参照渡しです。
「参照」とは「ポインタ」ともいいますが、そのデータのメモリ内の位置情報のことです。ですから、値をメモリ内の別の場所にコピーして渡しているのではないので、メモリを必要以上に消費しません。

次に、文書表示のローカルプロシージャDOC_DISPLAYの処理に焦点を当てて解説します。

まず最初に、19行目でTITLEタグに文書のタイトル(REC1.TITLE)を設定しているので、ブラウザのタイトルバー、あるいは、ページのタブにタイトルが表示されます。
そのタイトルは、実際にブラウザの表示内容であるBODYタグの範囲にも表示します(21~23行目)。そこではフォントの色を青(COLOR="BLUE")にしたり、書体をイタリック()にしたり、文書の更新日時をセットします。
このような表示については、自由にカスタマイズしたり、あるいは不要と判断するなら、記述しなくてもよいです。

しかし、文書表示ですから本文は必須ですね。
本文は改行コードを含みますので、PREタグで囲ってあります(25行目 <PRE>~</PRE>)。
ですから文書を登録するときに書いたそのままの状態で表示されます。ただし、PREタグで囲っているということは、その範囲にHTMLドキュメントのタグがあると、本来のタグの機能を正しく発揮できない場合がありますので注意してください。この注意点については回避策もあるので別の回で簡単に解説したいと思います。なお、本文(HONBUN列)は大量のテキストデータということで、DOCS表上、CLOB型ですが、本システムではCLOB型の値全体をPL/SQL変数経由で読み書きしている単純な方法なので、最大サイズは32kバイトになります。データベース側のキャラクタセットにもよりますが、仮に日本語1文字が3バイトだとしても、だいたい、10000文字(400字詰め原稿用紙25枚)でもOKという計算になります。

それでは表示してみましょう。
前回、私の環境では、文書番号=3 で最初の文書が登録されていますので、バックナンバー 第103回「WEBアプリ作成(1) (Oracle DBとPL/SQLだけで、即、WEBアプリ)」と同等の設定ができていれば、以下のURLで表示できます。
文書番号3なのでURLの最後の「?p_id=3」で番号を指定しています。

http://localhost:8080/dad/doc_show?p_id=3

ログイン画面が表示されたら、SCOTTユーザでログインします。

すると以下のように文書が表示されました。参考までに各部分の意味を書き添えました。

いかがですか? きちんと表示されていますね。

また、文書番号ではなく、ディレクトリ番号を表示するとディレクトリ表示のローカルプロシージャが以下のように表示するわけですね。

ということで、次回はディレクトリの表示機能について実装していきましょう。
ご期待ください。

先頭へ戻る