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

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

第126回「実用WEBアプリ 文書管理システム(7)更新処理」

2015.04.23

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

今回のテーマは「更新」です。

今まで、文書やディレクトリを「登録」したり、「表示」することについて解説をしてきました。しかし、それだけでは基本機能として不十分であり、当然文書やディレクトリの内容を「更新」することも必要ですね。例えば、いったん書いた文章を修正したり、ディレクトリの名前を変えたりするニーズはよくあることです。

更新処理の概要としては以下のようなものです。

まず、文章やディレクトリの番号を指定して、更新フォーム画面を生成するプロシージャを実行すると、該当する文書やディレクトリの項目がセットされたフォーム画面が開きます。その画面の項目を更新して送信ボタンをクリックすると、それらの値とパラメータ名が更新処理を行う別のプロシージャに送信され、そのプロシージャはそれらのパラメータの値を使って、データベースの表を更新するわけです。このように、2つのプロシージャ(画面と処理)でワンセットです。このパターンの処理はPL/SQLを使ったWEBアプリ構築で一般的なもので、今まで何度も解説してきましたね。

では、具体的なプロシージャ解説の前に、いつものように文書やディレクトリを格納する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_UPDATEとしました。

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

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
CREATE OR REPLACE PROCEDURE DOC_UPDATE
(P_ID IN VARCHAR2)
IS
  V_ID      DOCS.ID%TYPE;    -- NUMBER型
  REC       DOCS%ROWTYPE;    -- DOCS表の1行(1レコード)
  V_KBN_NAME  VARCHAR2(20);    -- '文書' または 'ディレクトリ';
  V_MSG1     VARCHAR2(20);
  V_ERRMSG  VARCHAR2(500);
BEGIN
/********************************************************/
/*   文書・ディレクトリの更新・削除のためのフォーム画面 */
/********************************************************/
   V_ID := TO_NUMBER(P_ID);   -- パラメータP_IDが数値であることのチェック兼ねる
   SELECT * INTO REC FROM DOCS WHERE ID = V_ID;
   IF REC.KBN = 1 THEN
      V_KBN_NAME := '文書' ;
      V_MSG1     := '(必須です)' ;   -- 文書の場合は親ディレクトリが必要であるメッセージのため
   ELSE
      V_KBN_NAME := 'ディレクトリ' ;
      V_MSG1     := NULL ;
   END IF;
   HTP.P( '<HTML>' );
   HTP.P( '<HEAD><TITLE>' || V_KBN_NAME || '更新</TITLE></HEAD>' );
   HTP.P( '<BODY>' );
   HTP.P( '<H1>' || V_KBN_NAME || '更新</H1>' );
   HTP.P( '<HR>' );    -- 横線
   HTP.P( '<FORM ACTION="doc_update_exe" METHOD="POST">' );
   HTP.P( '<TABLE BORDER>' );
   HTP.P( '<TR><TD BGCOLOR="LIGHTYELLOW">親ディレクトリのID</TD><TD>' ||
   '<INPUT TYPE="TEXT" NAME="P_OYA_ID" SIZE="8" VALUE="' || TO_CHAR(REC.OYA_DIR) ||
   '">' ||V_MSG1 || '</TD></TR>' );
   HTP.P( '<TR><TD BGCOLOR="LIGHTYELLOW">表示順番</TD><TD>' ||
   '<INPUT TYPE="TEXT" NAME="P_SEQ" SIZE="8" VALUE="' ||TO_NUMBER(REC.SEQ) ||
   '"> (同じディレクトリ内の表示順)</TD></TR>' );
   HTP.P( '<TR><TD BGCOLOR="LIGHTYELLOW">タイトル</TD><TD>' ||
  '<INPUT TYPE="TEXT" NAME="P_TITLE" SIZE="60" VALUE="' || REC.TITLE || '"></TD></TR>' );
   IF REC.KBN = 1 THEN    -- 文書のとき、本文を表示する
     HTP.P( '<TR><TD BGCOLOR="LIGHTYELLOW" COLSPAN="2"><CENTER>本文</CENTER></TD></TR>' );
     HTP.P( '<TR><TD COLSPAN="2">' ||
    '<TEXTAREA NAME="P_HONBUN" rows="15" cols="100">' ||
           REC.HONBUN ||
           '</TEXTAREA></TD><TR>' );
   ELSE
      NULL ;     -- ディレクトリの時は、本文を表示しない
   END IF;
   HTP.P( '</TABLE>' );
   -- パラメータ P_IDは画面上には項目がないので、隠しパラーメータで送る
   HTP.P( '<INPUT TYPE="HIDDEN" NAME="P_ID" VALUE="' ||P_ID || '">' );
   HTP.P( '<INPUT TYPE="SUBMIT" VALUE="送信">' ||
   '(タイトルに「削除」とセットして送信ボタンをクリックすると該当' || V_KBN_NAME || 'を削除します)' );
   HTP.P( '</FORM>' );
   HTP.P( '</BODY>' );
   HTP.P( '</HTML>' );
EXCEPTION
    WHEN VALUE_ERROR THEN
        HTP.P( '文書番号が数値ではありません' );
    WHEN NO_DATA_FOUND THEN
        HTP.P( '指定された番号が文書表(DOCS表)に存在しません' );
   WHEN  OTHERS  THEN
      V_ERRMSG := SQLERRM;
      HTP.P(V_ERRMSG);
END  DOC_UPDATE;
/
 
プロシージャが作成されました。

実は上記のプロシージャは、バックナンバー第122回「実用WEBアプリ 文書管理システム(3)文書登録」に掲載している、登録のためのフォーム画面プロシージャ(DOC_DOC_MAKE)とかなり似ています。つまり、更新のためのフォーム画面は登録のためのフォーム画面とよく似ているということです。
なぜかというと、「更新」も「登録」もその画面の項目はほとんど共通だからです。大きな違いは、「登録」画面の場合は開いたときに各項目は空の状態ですが、「更新」の場合は、画面が開いたときは、各項目はその対象となるデータの値がセットされた状態であるということです。

そういったことを意識しながら上記のソースコードの重要な部分を確認していきましょう。

まず、この「更新」フォーム画面のプロシージャには、「登録」の場合と違って、呼び出し時のパラメータがあります。2行目のP_IDがそのパラメータです。これがディレクトリや文書の番号の指定です。ディレクトリや文書の番号をこのパラメータで指定してプロシージャをコールします。

次のその番号(P_ID → V_ID)を条件値にしてDOCS表の1行を取得します(14行目 レコード変数 REC)。そして各項目はそのレコード変数の値をセットされた状態で生成されます。例えば、30行目、33行目、36行目などのINPUTタグにVALUE属性があり、その値が、「REC.列名」となっています(例 REC.OYA_DIR, REC.SEQ, REC.TITLE,)。また、TEXTAREAタグの開始<TEXTAREA ・・・>と終了</TEXTAREA>の間に、文書の本文があります(41行目 REC.HONBUN)。このように各項目に値があることが、「登録」の画面にはない特徴です。

また、指定された番号(P_ID)が文書である場合とディレクトリである場合で処理を分けている点にも注目してください。例えば、37行目のIF文ですが、文書の場合(REC.KBN = 1)は、本文の項目を生成しますがディレクトリ(REC.KBN=2)の場合は本文はありません。今回の前提として、「ディレクトリとは、本文を持たない文書」であるので、文書(REC.KBN=1)の場合のみ、本文の項目を持たせているわけです。

では次に、送信ボタンをクリックした場合の送信先である更新処理プロシージャ(上記ソースコード 27行目 doc_update_exeプロシージャ)について解説します。

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
CREATE OR REPLACE PROCEDURE DOC_UPDATE_EXE
( P_ID  IN  VARCHAR2,
   P_OYA_ID  IN VARCHAR2,
   P_SEQ     IN VARCHAR2,
   P_TITLE   IN VARCHAR2,
   P_HONBUN  IN VARCHAR2  DEFAULT NULL )
IS
   V_ERRMSG  VARCHAR2(1000);
BEGIN
/********************************************************/
/*   更新・削除処理                                     */
/********************************************************/
   IF P_TITLE = '削除' THEN
      DELETE FROM DOCS WHERE ID = TO_NUMBER(P_ID);
      IF SQL%ROWCOUNT = 1 THEN
         COMMIT ;
         HTP.P( 'ID=' ||P_ID || 'が削除されました' );
      ELSE
         ROLLBACK ;
         HTP.P( 'ID=' || P_ID || 'の削除が異常です。処理を取り消しました' );
      END IF;
   ELSE    -- 更新処理の場合
      UPDATE DOCS
         SET  TITLE = P_TITLE,
              HONBUN = P_HONBUN,
              OYA_DIR = TO_NUMBER(P_OYA_ID),
              SEQ     = TO_NUMBER(P_SEQ),
             UPDATE_DATE = SYSDATE
      WHERE ID = TO_NUMBER(P_ID);
      IF SQL%ROWCOUNT = 1 THEN
         COMMIT ;
         HTP.P( 'ID=' ||P_ID || 'が正常に更新されました
' );
         HTP.P( '<a href="doc_show?p_id=' || P_ID || '">表示する</a>
' ||
               '<a href="doc_update?p_id=' ||P_ID || '">更新画面へもどる</a>' );
      ELSE
         ROLLBACK ;
         HTP.P( 'ID=' || P_ID || 'の更新が異常です。処理を取り消しました' );
      END IF;
   END IF;
EXCEPTION
   WHEN OTHERS  THEN
        V_ERRMSG := SQLERRM;
        HTP.P(V_ERRMSG);
END DOC_UPDATE_EXE;
/
 
プロシージャが作成されました。

では上記の更新処理プロシージャについて解説します。

最初に、2~6行目のパラメータの記述で、6行目の「P_HONBUN」パラメータだけ、「DEFAULT NULL」の指定がある点に注目してください。この意味ですが、これは前の更新フォーム画面で、ディレクトリの場合は、本文の項目がないので、「P_HONBUN」パラメータが存在しないことがあり得るからです。
すなわち、リクエストパラメータに存在したり、存在しなかったりするパラメータがある場合は、存在しなかった場合のデフォルト値の指定が必要だということです。デフォルト値の指定がない場合、そのパラメータがリクエストの中に含まれていないとエラーとなってしまいます。そのエラーを防ぐためデフォルト値の指定をしています。

次にソースコードを見ておわかりと思いますが、13行目のIF文で P_TITLEパラメータの値が、'削除'である場合は、削除処理を行います(14行目の DELETE文)。つまりフォーム画面で項目「タイトル」の値が「削除」とセットされた場合は削除処理を行います。

本来、削除処理については、誤削除を防ぐために別画面を開いて、本当に削除するかどうかをYES/NOで確認してから削除するのが一般的で望ましい方法なのですが、実装がたいへん細かくなるので、今回は簡便な方法で削除可能としました。この方法でも誤削除を防ぐことはできます。
なお、ディレクトリの削除の場合は、その下の子供たち(文書やサブディレクトリ)が存在する場合は、外部キー制約違反エラーとなります。(バックナンバー120回でDOCS表を作成したとき、OYA_DIR列は、ID列を参照する再帰的な外部キーとしました)したがって、ディレクトリを削除する場合は、その下の文書やサブディレクトリから削除する必要があります。

「タイトル」の値が「削除」でないときは更新処理を行います(23~29行目 UPDATE文)。該当レコードが文書の場合でも、ディレクトリの場合でも、同じUPDATE文です。なぜなら、ディレクトリの場合は本文の値(P_HONBUN)がNULLであるという違いだけであり、UPDATE文の構文には影響しないからです。

削除(DELETE)であれ、更新(UPDATE)であれ、その処理の直後で、念のために処理された行数が1行であることを確認しています。(15行目、および、30行目 SQL%ROWCOUNT) そして万が一、その行数が1行でないときは、なにか想定外の異常な状況なので、処理を取り消して(ROLLBACK)エラーメッセージを表示します。

1行更新できた場合は、想定通りなので、確定(COMMIT)し、その文書またはディレクトリを表示するリンク(30行目)や、更新画面に戻るリンク(31行目)を表示します。

以上が更新処理のプロシージャの処理概要です。

では早速、実際に実行して確認してみましょう。

私の環境では、DOCS表のID=5の行が、以前のデモでつかった「桃太郎」の文書なので、その文章の更新画面を開きます。
バックナンバー 第103回「WEBアプリ作成(1) (Oracle DBとPL/SQLだけで、即、WEBアプリ)」と同等の設定ができていれば、以下のURLです。

http://localhost:8080/dad/doc_update?p_id=5

そうすると、文書の更新画面が開きます。

本文を一部、修正し送信ボタンをクリックします。

正常に更新された旨のメッセージが表示されました。その画面の「表示する」リンクをクリックします。

修正された「桃太郎」の文書が表示されました。

いかがですか? 文書の更新が可能ですね。ちなみに以下はディレクトリの番後を指定した場合ですが、文書の場合と違って、本文がないことがわかります。
ディレクトリには本文はありませんが、タイトルなどを更新すれば、ディレクトリのタイトル(名前)を修正できるわけです。

これで、文書やディレクトリの内容が更新可能になりました。

ここで次回につながるテーマとして注目していただきたい点があります。
それは、「親ディレクトリのID」の項目です。この項目はその文書やディレクトリの所属先の親ディレクトリの番号(OYA_DIR列)ですね。この項目が変更できるということは、所属先のディレクトリを変更できるということです。しかし、上記のコードのままでは以下のようなおかしな更新を許すことになります。

1. 親ディレクトリのIDが、ディレクトリでなく文書のIDである
2. 親ディレクトリのIDが、自分よりも下の階層のディレクトリのIDである(循環するディレクトリ)

ここで1.については容易にチェックできますが、2.についてはチェックする階層の深さに制限がないので単純ではありません。

次回はこの2つの問題を防ぐように、DOC_UPDATE_EXEプロシージャを修正したいと考えています。
ご期待ください。

先頭へ戻る