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

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

第137回「実用WEBアプリ 文書管理システム(18)複数ディレクトリ・文書の一括削除、整列処理(1)」

2016.07.14

こんにちは、インストラクターの蓑島です。
本連載、しばらくご無沙汰しておりましたが、再開いたします。

その前に少し間が空きましたので、本メルマガの概要を簡単におさらいしておきます。

本メルマガ「PL/SQLを使ってみよう」は前半部分(第1回~第109回)では、PL/SQL文法解説とコーディング例がメインであり、後半部分(第103回~)は、応用例を中心に解説しています。そして応用例の「実用WEBアプリ」シリーズでは、PL/SQLだけで、実用的なWEBアプリの作成ができることを解説しています。通常、WEBアプリは、さまざまな技術の組み合わせです。例えば、データベースサーバー、中間層(アプリケーションサーバーなど)、開発言語としてJava、Perl、PHP、Ruby、など様々な技術を組み合わせます。しかし、Oracleデータベースであれば、そこに備わっているPL/SQL言語だけで、他になにも必要とせず、簡単にWEBアプリを作成できます。(バックナンバー第103回 「Oracle DBとPL/SQLだけで、即、WEBアプリ」参照)
バックナンバーに掲載しているソースコードを皆さんの環境(SQL*PlusやSQL*Developer)で順番通りに実行すれば、簡単にWEBアプリを作成できます。「実用WEBアプリ」シリーズで使っているOracleデータベースのバージョンは無償版の Oracle 11g Express ですから、誰でも簡単に試せる内容となっています。

※バックナンバーは以下のページから参照できます。
https://www.istudy.ne.jp/blog/tag/plsql

「実用WEBアプリ」シリーズで毎回少しずつ作りこんでいるのは、文書管理システムです。WEBブラウザを通して、Oracleデータベース内に、ディレクトリを作成したり、文書を作成したり、表示、更新、削除、検索したりできます。

前回の第136回は、そのような文書管理システムで、チェックボックスの複数選択によるディレクトリや文書の一括移動を可能にするコーディングをご紹介しました。

今回は、さらに機能を加え、複数ディレクトリや文書の一括削除、同一ディレクトリ内の表示順番の一括更新などを加えたいと思います。これらの機能の追加で、本文書管理システムを便利にナビゲートできるので、かなり使いやすく、ほぼ完成に近いレベルになります。

では早速、実装例を表示します。前回と同じように、ディレクトリや文書の表示処理 doc_showプロシージャをブラウザから開きます。私の環境では、id=8のディレクトリを表示して解説します。

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

PLSQL137_02

この画面は、DOSC表(定義は、バックナンバー第103回など参照)の ID = 8 のレコードを対象にしているのですが、ID = 8 のレコードがディレクトリ(KBN = 2)なので、プログラム処理により、そのディレクトリ以下のサブディレクトリや文書の一覧を表示しているものです。つまり「PL/SQLの勉強ノート」というディレクトリ以下を表示しているわけですね。

ここで、赤で囲ったところが、前回からみて追加されている部分です。つまり、「表示順番」という項目と、「整列」ボタン、「更新ボタン」、「削除」ボタンが追加されている部分です。

この「表示順番」列は、DOCS表のSEQ列に相当する項目として用意しました。ここに表示したい順番に数値を入力して、「更新」ボタンをクリックすると、その通りの順番に一括で並び替えることができます。実際にやってみましょう。値は数値であれば何でもよい(DOCS.SEQ列はNUMBER1型だから)ので、マイナスや小数点があってもOKです。



確かに、入力した順に並び変わったことがわかります。ここで、表示順番に入力した値はマイナスがあったり、小数点がついていたりしますが、いずれにしろ、数値の大きさに並んでいますね。ということで、並び替え処理を一括で可能です。この例のように、表示順のため、マイナスの数値を使ったり、小数点をつけることもあり得ると思います。しかし、どのような数値を入力しても、その順番どおりに、10,20,30……ときれいな値に整列できれば、見やすいし便利ですね。「整列」ボタンはそのためのボタンです。

「整列ボタン」をクリックすると……


ご覧のように、表示順番がきれいに整列できました。

以上が、「表示順番」に関する一括更新処理です。

それでは、次に、テーマを変えて、チェックボックスで複数選択し、一括削除する処理です。表示する画面は先ほどと同じです。

この画面では、ID=22の文書と、ID=14、ID=20のディレクトリを選択して、削除ボタンをクリックするのですが、実は私の環境では、ID=14については、子レコードのあるディレクトリとなっています。

ご覧のように、子レコードのあるディレクトリは削除できません。外部キー制約があるからです。(バックナンバー第120回の、CREATE TABLE DOCSの 6行目 「OYA_DIR NUMBER REFERENCES DOCS(ID)」で自分自身の表を参照する外部キー制約を宣言しています。)

そこで、子レコードのあるディレクトを除いて、削除してみます。



今度は、削除できました。もしも、子レコードのあるディレクトリを削除したければ、そのディレクトリに移動し、今回の機能を使って、子レコードを一括削除すればいいので、面倒なことではありません。

では、このような機能をプログラムレベルで解説してきます。今回はソースコードの掲載だけとし、次回に、前回と比べてどのようにソースコードを修正しているかを解説します。前回第136回までのソースコードを実行している方は、以下のコードを実行することで今回の機能を反映できます。

最初に、表示画面を生成するプロシージャ DOC_SHOWです。

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

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

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
create or replace
PROCEDURE DOC_SHOW
(P_ID  IN VARCHAR2 DEFAULT NULL )
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(DOC_FUNC_MENU(REC1.ID));
      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
      TYPE CUR_DOCS_TYPE IS REF CURSOR ;   -- 型の宣言
      CUR_DOCS   CUR_DOCS_TYPE;           -- その型を使って、カーソル変数を宣言
      REC_DOCS DOCS%ROWTYPE;
      V_TITLE  DOCS.TITLE%TYPE;
   BEGIN
   /************************************************/
   /*   動的SQLを使った、明示カーソルのオープン    */
   /************************************************/
      IF REC2.ID IS NULL THEN  -- パラメータが指定されていないとき、(OYA_DIR IS NULL)
         OPEN CUR_DOCS FOR 'SELECT * FROM DOCS WHERE OYA_DIR IS NULL ORDER BY SEQ' ;
         V_TITLE := '最上位ディレクトリの一覧' ;
      ELSE  -- パラメータが指定されているとき (OYA_DIR = :1)
         OPEN CUR_DOCS FOR 'SELECT * FROM DOCS WHERE OYA_DIR = :1 ORDER BY SEQ' USING REC2.ID;
         V_TITLE := REC2.TITLE;
      END IF;
      HTP.P( '<HTML>' );
      HTP.P( '<HEAD><TITLE>' || V_TITLE|| '</TITLE></HEAD>' );
      HTP.P( '<BODY>' );
      -- メニューバーの追加
      HTP.P(DOC_FUNC_MENU(REC2.ID));
      -- ディレクトリのタイトルを表示
      HTP.P( '<FONT COLOR="BLUE"><I>' || V_TITLE || '</I></FONT>' );
      -- このディレクトリ以下の文書とサブディレクトリを表示
      -- 表示する文章とサブディレクトリ全体をFORMタグで囲む  2015/08/06 ★★★
      HTP.P( '<FORM name="fname" ACTION="doc_dir_man_exe" METHOD="POST">' );
      HTP.P( '<INPUT TYPE="HIDDEN" NAME="P_SEL_ID" VALUE="DUMMY">' );
      HTP.P( '<INPUT TYPE="HIDDEN" NAME="P_SEQ" VALUE="DUMMY">' );   -- ★★★
      HTP.P( '<INPUT TYPE="HIDDEN" NAME="P_ID" VALUE="DUMMY">' );   -- ★★★
      HTP.P( '<TABLE BORDER>' );
      HTP.P( '<TR BGCOLOR="LIGHTYELLOW"><TD>ID</TD><TD>#</TD><TD>タイトル</TD><TD>更新日</TD>' ||
      -- ★★2016/05 追加 ★★
      '<TD>表示順番</TD>' ||
      '<TD><INPUT TYPE="CHECKBOX" NAME="aaa" onClick="AllChecked();" /></TD>' ||
      '</TR>' );
   /************************************************/
   /*   カーソル変数を使った、行の取り出し( FETCH )  */
   /************************************************/
      LOOP
         FETCH CUR_DOCS INTO REC_DOCS;
         EXIT WHEN CUR_DOCS%NOTFOUND;
         HTP.P( '<TR>' ||
               '<TD>' || TO_CHAR(REC_DOCS.ID) || '</TD>' ||
               '<TD>' || CASE REC_DOCS.KBN  WHEN 1 THEN '文' ELSE  '▼' END || '</TD>' ||
               '<TD><A href="doc_show?p_id=' || TO_CHAR(REC_DOCS.ID) || '">' || REC_DOCS.TITLE || '</A></TD>' ||
               '<TD>' || TO_CHAR(REC_DOCS.UPDATE_DATE, 'YYYY/MM/DD HH24:MI' ) || '</TD>' ||
      -- ★★2016/05 追加 ★★
               '<TD><INPUT TYPE="TEXT" NAME="P_SEQ" VALUE="' || TO_CHAR(REC_DOCS.SEQ ) || '">' ||
     -- ★★2016/05 追加 ★★
               '<INPUT TYPE="HIDDEN" NAME="P_ID" VALUE="' || TO_CHAR(REC_DOCS.ID) || '"></TD>' ||
               '<TD><INPUT TYPE="CHECKBOX" NAME="P_SEL_ID" VALUE="' ||TO_CHAR(REC_DOCS.ID) || '"></TD>' ||
               '</TR>' );
      END LOOP;
      CLOSE CUR_DOCS;
      HTP.P( '</TABLE>' );
      HTP.P( '表示順番を10,20,30,・・のように<INPUT TYPE="SUBMIT" NAME="P_BUTTON" VALUE="整列">します。<BR>' );
      HTP.P( '表示順番を指定した通りに一括で<INPUT TYPE="SUBMIT" NAME="P_BUTTON" VALUE="更新">します。<BR>' );
      HTP.P( 'チェックしたディレクトリや文書をディレクトリ番号【<INPUT TYPE="TEXT" SIZE="5" NAME="P_OYA_DIR">】に<INPUT TYPE="SUBMIT" NAME="P_BUTTON" VALUE="移動">します。<br>' );
      HTP.P( 'チェックしたディレクトリや文書を一括で<INPUT TYPE="SUBMIT" NAME="P_BUTTON" VALUE="削除">します。<BR>' );
       HTP.P( '</FORM>' );    -- フォームタグの終わり  ★★★
       HTP.P( '<script language="JavaScript" type="text/javascript">
<!--
function AllChecked(){
   var check =  document.fname.aaa.checked;
   for (var i=0; i<document.fname.P_SEL_ID.length; i++){
      document.fname.P_SEL_ID[i].checked = check;
   }
}
//-->
</script>' );
  HTP.P( '</BODY>' );
  HTP.P( '</HTML>' );
   END DIRECTORY_DISPLAY;
/*****************************************************************/
/*   実行部                                                      */
/*****************************************************************/
BEGIN
   /********************/
   /* チェック         */
   /********************/
   IF P_ID IS NULL THEN    --番号が指定されていなければ
      DIRECTORY_DISPLAY( NULL );     --最上位のディレクトリ一覧を表示する
   ELSE                    --番号が指定されていれば
       V_ID := TO_NUMBER(P_ID);     --その番号を数字に変換
                           -- その番号でDOCS表の1レコード(行)を取得
       SELECT * INTO REC  FROM DOCS WHERE ID = V_ID;
       /**************************************************************/
       /* その1レコードの区分(KBN)に応じて文書またはディレクトリを表示 */
       /**************************************************************/
       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;
   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 ;
/
 
プロシージャが作成されました。

次に実際に処理を行うプロシージャ DOC_DIR_MAN_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
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
create or replace
PROCEDURE DOC_DIR_MAN_EXE (P_BUTTON IN VARCHAR2,  -- クリックしたボタンの名前
                            P_OYA_DIR IN VARCHAR2,  -- 移動先のディレクトリ番号
                            P_SEL_ID  IN OWA.VC_ARR, -- チェックした文書やディレクトリの番号
                            P_SEQ     IN OWA.VC_ARR, -- 表示順番の配列
                            P_ID      IN OWA.VC_ARR, -- チェックしたDOCS表の主キーの配列
                            aaa       IN VARCHAR2 DEFAULT NULL )
IS
   USER_ERROR  EXCEPTION;
   V_ERR_MSG   VARCHAR2(800);
   V_ERROR_EXISTS  BOOLEAN := FALSE ;
   FK_ERROR    EXCEPTION;
   PRAGMA EXCEPTION_INIT(FK_ERROR,-2292);
   FK_ERROR_ID   NUMBER;
BEGIN
   IF P_BUTTON = '移動' THEN   -- 一括ディレクトリ移動処理
      IF P_SEL_ID. COUNT = 1 THEN  -- COUNTが1ということはDUMMYのみなので、一つも選択されていない
         V_ERR_MSG := '移動する文書やディレクトリをチェックしてください' ;
         RAISE USER_ERROR;
      END IF;
      IF P_OYA_DIR IS NULL THEN
         V_ERR_MSG := '移動先のディレクトリが指定されていません<BR>' ;
         V_ERR_MSG := V_ERR_MSG ||
                '最上位のディレクトリとして移動したいときは個別の更新画面から親ディレクトリIDをNULLにしてください' ;
         RAISE USER_ERROR;
      END IF;
      -- 以下はディレクトリ移動処理
    FOR I IN 2 .. P_SEL_ID. LAST  LOOP  -- ループ処理
       DECLARE    -- ネストしたブロック
            V_SQLCODE  NUMBER;
       BEGIN
        -- 移動先のディレクトリチェック(循環しないなど)
         IF DOC_FUNC_OYA_CHECK(TO_NUMBER(P_SEL_ID(I)),TO_NUMBER(P_OYA_DIR)) THEN
            -- OKであれば、ディレクトリを移動させる
            UPDATE DOCS  SET OYA_DIR = TO_NUMBER(P_OYA_DIR)
            WHERE  ID = TO_NUMBER(P_SEL_ID(I));
         END IF;
       EXCEPTION
         WHEN NO_DATA_FOUND THEN
                 V_ERROR_EXISTS := TRUE ;
                 V_ERR_MSG := '移動先番号の番号が存在しません。 移動先番号=' || P_OYA_DIR;
                 RAISE USER_ERROR;
         WHEN OTHERS THEN
              V_SQLCODE := SQLCODE;
              IF V_SQLCODE  = -20000  THEN
                 V_ERROR_EXISTS := TRUE ;
                 HTP.P( '文書の親ディレクトリ番号をNULLにできません 番号=' ||P_SEL_ID(I) || '<BR>' );
              ELSIF  V_SQLCODE = -20001 THEN
                 V_ERROR_EXISTS := TRUE ;
                 V_ERR_MSG := '移動先番号がディレクトリでなく文書です 移動先番号=' || P_OYA_DIR;
                 RAISE USER_ERROR;
              ELSIF V_SQLCODE = -20002 THEN
                V_ERROR_EXISTS := TRUE ;
                HTP.P( 'ディレクトリが循環します 番号=' ||P_SEL_ID(I));
              ELSE
                  V_ERROR_EXISTS := TRUE ;
                  RAISE;
              END IF;
      END ;   -- ネストしたブロックの終わり
   END LOOP;
   HTP.P( '選択された文書・ディレクトリを指定されたディレクトリに移動しました' );
   ELSIF P_BUTTON = '更新' THEN  -- 表示順番の更新処理
      IF P_SEQ. COUNT = 1 THEN
         V_ERR_MSG := 'このディレクトリ以下には該当する子レコードがありません' ;
         RAISE USER_ERROR;
      END IF;
      
      FOR I IN 2..P_SEQ. COUNT LOOP
          UPDATE DOCS SET SEQ = P_SEQ(I) WHERE ID = P_ID(I);
      END LOOP;
      COMMIT ;
      HTP.P( '表示順の更新処理が完了しました' );
   ELSIF   P_BUTTON = '整列' THEN
     IF P_SEQ. COUNT = 1 THEN
         V_ERR_MSG := 'このディレクトリ以下には該当する子レコードがありません' ;
         RAISE USER_ERROR;
      END IF;
      
      DECLARE   -- ネストブロック
          V_NUM   NUMBER := 10;
      BEGIN
         FOR I IN 2..P_SEQ. COUNT LOOP
              UPDATE DOCS SET SEQ =V_NUM WHERE ID = P_ID(I);
              V_NUM := V_NUM + 10;
         END LOOP;
         COMMIT ;
       END ;   -- ネストブロックの終わり
      HTP.P( '順序番号を10,20,30,・・・のように整列しました' );
   ELSIF  P_BUTTON = '削除' THEN
       IF P_SEL_ID. COUNT = 1 THEN  -- COUNTが1ということはDUMMYのみなので、一つも選択されていない
          V_ERR_MSG := '削除する文書やディレクトリをチェックしてください' ;
          RAISE USER_ERROR;
       END IF;
       FOR I IN 2..P_SEL_ID. COUNT LOOP
           FK_ERROR_ID := P_SEL_ID(I);    -- ID番号を保存しておいてからDELETE
           DELETE FROM DOCS WHERE ID = P_SEL_ID(I);
       END LOOP;
       HTP.P( '削除処理が完了しました' );
       COMMIT ;
   ELSE
      V_ERR_MSG := 'P_BUTTONの値が"移動"ではありません' ;
      RAISE USER_ERROR;
   END IF;
   
EXCEPTION
   WHEN USER_ERROR THEN
        IF  V_ERROR_EXISTS THEN
           ROLLBACK ;
        END IF;
        HTP.P(V_ERR_MSG);
   WHEN FK_ERROR  THEN
        HTP.P( 'ID=' || TO_CHAR(FK_ERROR_ID) || 'は、子レコードのあるディレクトリです。先に子レコードを削除してください' );
        ROLLBACK ;
   WHEN OTHERS  THEN
        V_ERR_MSG := SQLERRM;
        ROLLBACK ;
        HTP.P(V_ERR_MSG);
END  DOC_DIR_MAN_EXE;
/
 
プロシージャが作成されました。

それでは今回はここまでとします。
次回、上記のソースコードを解説します。ご期待ください。

先頭へ戻る