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

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

第124回「実用WEBアプリ 文書管理システム(5)ディレクトリ表示」

2015.04.09

こんにちは。インストラクターの蓑島です。
春になったと思ったら、また寒い日が続いたりしますね。早く初夏のようなさわやかさになってもらいたいものです。

前回は、文書(および、ディレクトリ)を表示するプロシージャ(DOC_SHOW)を作成しました。
このプロシージャは指定された番号が文書の番号である場合は、そのプロシージャ内の文書表示を行うローカルプロシージャ(DOC_DISPLAY)をコールし、その文書の内容を表示します。そして指定された番号がディレクトリである場合は、ディレクトリ表示を行うローカルプロシージャ(DIRECTORY_DISPLAY)をコールするのですが、このプロシージャの処理内容がメッセージ表示(『ディレクトリ表示機能は次回、実装予定です』と表示する)のみでした。したがって、実質的にまだ未実装の状態です。そこで今回、その機能を実装するわけです。

ディレクトリ表示機能の具体的な内容は以下のようにしましょう。

まず、そのディレクトリのタイトルを1行表示します。
次に、そのタイトルの下に、一覧表形式でそのディレクトリ以下の文書やサブディレクトリを表示します。つまり、ヘッダー・ディテールのように1対多の関係で、親ディレクトリのタイトルとその下の子供たち(文書やサブディレクトリ)を表示するわけです。
そのとき、それぞれの文書やサブディレクトリのタイトルに、それぞれの番号で、DOC_SHOWプロシージャをコールするリンクを張っておきます。(<A HREF="doc_show?p_id=nnn">タイトル</A>)
それにより、そのタイトルをクリックすると、それが文書の場合は前回実装した機能でその文書を表示します。あるいはサブディレクトリの場合は今回実装する機能でさらにその下の文書やサブディレクトリの一覧を表示できるわけです。つまりサブディレクトリのタイトルをクリックすればさらにそのサブディレクトリの下に移動するような感覚です。このようにディレクトリをどんどんと下方向にドリルダウンしながら、表示したい文書を表示できます。

では、ソースコードの解説の前にいつものように、文書やディレクトリを格納する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             --更新日

次に今回のテーマであるディレクトリ表示機能を実装したDOS_SHOWプロシージャのソースコードです。35~56行目の間が今回実装した部分です。

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

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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( '<HTML>' );
      HTP.P( '<HEAD><TITLE>' || REC2.TITLE || '</TITLE></HEAD>' );
      HTP.P( '<BODY>' );
      -- ディレクトリのタイトルを表示
      HTP.P( '<FONT COLOR="BLUE"><I>' ||REC2.TITLE || '</I></FONT>'
      || '<FONT SIZE="-1">['   --タイトルの横に更新日時をセット
      ||TO_CHAR(REC2.UPDATE_DATE, 'YYYY/MM/DD HH24:MI' ) || ']</FONT><BR>' );
      -- このディレクトリ以下の文書とサブディレクトリを表示
      HTP.P( '<TABLE BORDER>' );
      HTP.P( '<TR BGCOLOR="LIGHTYELLOW"><TD>ID</TD><TD>#</TD><TD>タイトル</TD><TD>更新日</TD></TR>' );
      FOR REC3 IN ( SELECT * FROM DOCS WHERE OYA_DIR = REC2.ID ORDER BY SEQ) LOOP
         HTP.P( '<TR>' ||
               '<TD>' || TO_CHAR(REC3.ID) || '</TD>' ||          -- ID
               '<TD>' || CASE REC3.KBN  WHEN 1 THEN '文' ELSE  '▼' END || '</TD>' ||  -- 区分
               -- タイトルにリンクを張っている
               '<TD><A href="doc_show?p_id=' || TO_CHAR(REC3.ID) || '">' || REC3.TITLE || '</A></TD>' ||
               '<TD>' || TO_CHAR(REC3.UPDATE_DATE, 'YYYY/MM/DD HH24:MI' ) || '</TD>' ||   -- 更新日
               '</TR>' );
      END LOOP;
      HTP.P( '</TABLE>' );
      -- ▲▲▲ 今回追加した処理 ここまで  ▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲▲
   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 ;
/
 
プロシージャが作成されました。

解説します。

最初に指定されたディレクトリのタイトルを表示します(37行目、および40~42行目)。この部分は文書表示のローカルプロシージャとまったく同じですね。

次に指定されたディレクトリ以下の文書やサブディレクトリを表形式で表示します(44~55行目)。ここで特に大事なのは、46~54行目のカーソルFORループ文です。ここの46行目のSELECT文のWHERE句に注目してください。

SELECT * FROM DOCS WHERE OYA_DIR = REC2.ID ORDER BY SEQ

このREC2.IDは、ディレクトリ表示用のローカルプロシージャDIRECTORY_DISPLAYに渡されたREC2パラメータ(DOCS表の該当する1行を格納)のID列を表します。つまりこのWHERE句の条件により、指定されたディレクトリを親とする子供の文書やサブディレクトリを問い合わすことができるわけです。そこで問い合わされた行をREC3というレコード変数に格納し(46行目)、表形式の各列の値として使用しているわけです。(例 REC3.ID、REC3.TITLE、REC3.UPDATE_TIME)

その値として少しわかりにくいのが、49行目の 「CASE ~ END」の記述です。この部分はCASE式という文法です。式なので、全体でなんらかの一つの値を表します。

49行目(抜粋) CASE REC3.KBN WHEN 1 THEN '文' ELSE '▼' END

このCASE式は、REC3.KBNの値が1であるときは、'文'という文字を返し、それ以外の場合は'▼'という文字を返します。よって、このCASE式は、'文'または'▼'という文字になります。つまり問い合わされた1行が文書(KBN=1)の場合は、'文'という文字を表示し、ディレクトリ(KBN=2)の場合は'▼'という文字を表示するわけです。「▼」という記号はその下にさらに階層があることを表現しているつもりです。

次に注目していただきたいのが51行目の以下の記述です。

51行目のアンカータグ <A href="doc_show?p_id=' || TO_CHAR(REC3.ID) ||'">

この部分はタイトルに貼ってあるリンクの記述ですが、問い合わされたその行のIDの値(REC3.ID)が仮に「5」だとすると、アンカータグ(A)のhref属性は次のようになります。「href="doc_show?p_id=5"」ここで、doc_showとはまさに今説明している文書やディレクトリを表示するDOC_SHOWプロシージャのことですね。ですからタイトルが文書のタイトルなのであれば、それをクリックするとその文書の内容が表示されます。またディレクトリのタイトルなのであればさらにそのディレクトリ以下の文書やサブディレクトリの一覧となります。

これで全体像がつかめたと思いますので、早速実行してみましょう。今回このデモのためにいくつかディレクトリと文書を事前に登録しました。本システムの最初のディレクトリは必ず「ID=1」ですから、1番を指定して実行します。 バックナンバー 第103回「WEBアプリ作成(1) (Oracle DBとPL/SQLだけで、即、WEBアプリ)」と同等の設定ができていれば、以下のURLで表示できます。

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

ログインを求められたらスキーマユーザ(SCOTT)でログインします。

ご覧のように、ID=1のディレクトリ以下の文書やサブディレクトリが表示されます。ここで、私の環境では「〇〇太郎が登場する日本の昔話」という事前登録したディレクトリがありますので、そのタイトルをクリックします。この行は、「▼」のマークがついていますので、ディレクトリであることがわかります。

そうすると、「桃太郎」、「浦島太郎」、「金太郎」という3つの文書があることがわかります。ここで「桃太郎」のタイトルをクリックします。

タイトルをクリックするとご覧のように、最終的に「桃太郎」の文書が表示されました。

いかがですか? 指定されたディレクトリから下のサブディレクトリに移動し、クリックした文書を表示できましたね。
これで今回の実装は一応完成です。また、すでに説明済みですが、どの画面もURLを見ると、DOC_SHOWプロシージャに対するリクエストです。つまり、DOC_SHOWプロシージャはディレクトリの番号を指定されれば、そのディレクトリ以下の一覧を表示し、文書の番号を指定されればその文書の内容を表示するわけですね。

では、ここで次につながる課題です。
このDOC_SHOWプロシージャで、親ディレクトリのIDを指定しなければ、最上位ディレクトリの一覧が表示できるでしょうか?当然ありえるニーズですね。しかし結論からいうと、上記のDOC_SHOWプロシージャでは最上位ディレクトリの一覧は表示できません。

このシステムの前提で、最上位ディレクトリは親ディレクトリがないディレクトリ(OYA_DIRがNULL)です。当然、複数の最上位ディレクトリが作成可能です。しかし、DOC_SHOWプロシージャは、親ディレクトリのIDを指定することで、その下のサブディレクトリや文書の一覧が表示できるので、親ディレクトリの指定は省略できません。つまり親ディレクトリの存在しないディレクトリ(最上位ディレクトリ)をさらにその上の視点から一覧に表示することはできないのです。
仮に、「doc_show?p_id=」のように、親ディレクトリの指定を省略するためにp_idの値を指定しないと、67行目の SELECT INTO文が0件でNO_DATA_FOUND例外となり、83行目のメッセージ「指定された番号が文書表(DOCS表)に存在しません」が表示されます。たとえこのエラーを回避するように修正しても、さらにもっと根本的な問題があります。それは、もし最上位ディレクトリ、つまり親ディレクトリのないディレクトリを問い合わせるなら、46行目のカーソルFORループ文のSELECT文を以下のように「構文的に」修正しなければならないということです。

46行目のSELECT文
(修正前) SELECT * FROM DOCS WHERE OYA_DIR = REC2.ID ORDER BY SEQ
(修正後) SELECT * FROM DOCS WHERE OYA_DIR IS NULL ORDER BY SEQ

注目してもらいたいのはWHERE句の条件です。OYA_DIRが NULLの行を問い合わせるためは、「OYA_DIR IS NULL」という異なる比較演算子の条件でなければなりません。つまり構文が変わるのです。例えば、親ディレクトリの指定を省略して、REC2.IDの値がNULLであったとしても、それだけでは「OYA_DIR = NULL」という条件となり、「OYA_DIR IS NULL」ではないので、結果は0件です。(SELECT INTO文ではなく、明示カーソル処理なので0件でもエラーにはなりませんが、とにかく問い合わせできません)

つまり、最上位ディレクトリの一覧を表示するためには、SELECT文のWHERE句の構文を変える必要があります。

そのために各構文毎のカーソルを用意することは、よい方法ではありません。実質的に同じ処理を2つ記述することになるので、非効率的です。あるいはプログラミングを工夫して、カーソルからフェッチした以降の処理をサブルーチン化すれば、同じ処理を2つ記述しなくても対応可能ですが、プログラムが少し複雑になります。この場合、一番素直な方法は「動的SQL」を使用することです。「動的SQL」を使えば一つのカーソル処理で対応できます。

ということで、次回は動的SQLを使用して、パラメータp_idを省略した場合は、最上位ディレクトリの一覧を表示するように修正したいと思います。その際に バックナンバー第46回 「動的SQL(複数行返す問い合わせの場合)」が参考になりますので興味のある方は事前に参照してみてください。

では、次回ご期待ください。

先頭へ戻る