RM-BLOG

IT系技術職のおっさんがIT技術とかライブとか日常とか雑多に語るブログです。* 本ブログに書かれている内容は個人の意見・感想であり、特定の組織に属するものではありません。/All opinions are my own.*

【ORACLE】DDLの取得

テーブル・マテビュー・INDEXのDDLを取得して、テーブル(マテビュー)別にファイルに吐きだすスクリプト
DBMS_METADATA.GET_DDLDBMS_METADATA.GET_DEPENDENT_DDLをカーソルと併用して使用し、
オブジェクト毎にDDLを取得しつつ、ファイルへの出力はUTL_FILEパッケージで行う。



 

declare 
    cursor CUR_TARGET_TABLES is
    select tab_name
    from (
          -- ↓ここに調査対象となるテーブルを並べる
          -- 全テーブルならUSER_TABLESで良い
          select 'TABLE_NAME1' tab_name from dual
          union all
          select 'TABLE_NAME2' tab_name from dual
    );
    
    REC_TARGET_TABLES CUR_TARGET_TABLES%ROWTYPE;

    cursor CUR_TARGET_OBJ_INDEXES(target_tab_name REC_TARGET_TABLES.TAB_NAME%TYPE) is
    select ui.index_name
    from user_indexes ui
    where ui.table_name = target_tab_name
      and not exists (
          select 1
          from USER_CONSTRAINTS uc
          where uc.TABLE_NAME = target_tab_name
            and uc.CONSTRAINT_TYPE = 'P'
            and uc.CONSTRAINT_NAME = ui.INDEX_NAME
          )
    ;

    REC_TARGET_OBJ_INDEXES CUR_TARGET_OBJ_INDEXES%ROWTYPE;

    NMB_CHECK_TAB number;
    NMB_CHECK_MVW number;
    NMB_CHECK_IDX number;
    NMB_CHECK_COM number;
    CHR_TARGET_OBJ_TYPE varchar2(30);

    CHR_TARGET_OBJ_DDL_FILENAME varchar2(50);
    TYP_FILE_HUNDLE UTL_FILE.FILE_TYPE;
    CHR_FILE_PATH varchar2(100) := 'REEEEEEEEEMAN_TEST_DIR';
    CHR_DDL varchar2(32767);
    CHR_IDX_DDL varchar2(32767);
    CHR_COM_DDL varchar2(32767);

begin

     -- セミコロン出力を有効にする
     DBMS_METADATA.SET_TRANSFORM_PARAM(
         TRANSFORM_HANDLE => DBMS_METADATA.SESSION_TRANSFORM,
         NAME => 'SQLTERMINATOR', VALUE => TRUE);

     open CUR_TARGET_TABLES;
     loop
         -- 変数初期化
         NMB_CHECK_TAB := 0;
         NMB_CHECK_MVW := 0;
         NMB_CHECK_IDX := 0;
         NMB_CHECK_COM := 0;
         CHR_TARGET_OBJ_TYPE := '';

         CHR_DDL := '';
         CHR_IDX_DDL := '';
         CHR_COM_DDL := '';

         fetch CUR_TARGET_TABLES into REC_TARGET_TABLES;
         exit when CUR_TARGET_TABLES%NOTFOUND;
             --DBMS_OUTPUT.PUT_LINE('TABLE_NAME=' || REC_TARGET_TABLES.TAB_NAME);

             -- テーブルとして存在するかチェック
             select count(1) cnt
             into NMB_CHECK_TAB
             from user_tables
             where table_name = REC_TARGET_TABLES.TAB_NAME;
             if NMB_CHECK_TAB > 0 then
                 -- マテビューとして存在するかチェック
                 select count(1) cnt
                 into NMB_CHECK_MVW
                 from user_mviews
                 where MVIEW_NAME = REC_TARGET_TABLES.TAB_NAME;
                 if NMB_CHECK_MVW = 0 then
                     -- テーブルとして定義を取得
                     CHR_TARGET_OBJ_TYPE := 'TABLE';

                     -- ファイル名を生成
                     CHR_TARGET_OBJ_DDL_FILENAME := 'DDL_TAB_' || REC_TARGET_TABLES.TAB_NAME || '.sql';
                 else
                     -- マテビューとして定義を取得
                     CHR_TARGET_OBJ_TYPE := 'MATERIALIZED_VIEW';

                     -- ファイル名を生成
                     CHR_TARGET_OBJ_DDL_FILENAME := 'DDL_MVW_' || REC_TARGET_TABLES.TAB_NAME || '.sql';
                 end if;

                 -- DDL取得(テーブルorマテビュー)
                 select DBMS_METADATA.GET_DDL(CHR_TARGET_OBJ_TYPE,REC_TARGET_TABLES.TAB_NAME)
                 into CHR_DDL
                 from dual;

                 CHR_DDL := replace(CHR_DDL,'"','');

                 -- DDL書き出し(テーブルorマテビュー)
                 TYP_FILE_HUNDLE := UTL_FILE.FOPEN(CHR_FILE_PATH,CHR_TARGET_OBJ_DDL_FILENAME,'W',32767);
                 UTL_FILE.PUT_LINE(TYP_FILE_HUNDLE,CHR_DDL);
                 UTL_FILE.NEW_LINE(TYP_FILE_HUNDLE,1);

                 -- 対象オブジェクトのインデックス数を取得
                 select count(1) cnt into NMB_CHECK_IDX
                 from user_indexes
                 where table_name = REC_TARGET_TABLES.TAB_NAME;
                 -- インデックスがあればそのDDLも取得して書き出し
                 if NMB_CHECK_IDX > 0 then
                     open CUR_TARGET_OBJ_INDEXES(REC_TARGET_TABLES.TAB_NAME);
                     loop
                         fetch CUR_TARGET_OBJ_INDEXES into REC_TARGET_OBJ_INDEXES;
                         exit when CUR_TARGET_OBJ_INDEXES%NOTFOUND;
                         -- DDL取得(インデックス)
                         select DBMS_METADATA.GET_DDL('INDEX',REC_TARGET_OBJ_INDEXES.INDEX_NAME)
                         into CHR_IDX_DDL
                         from dual;

                         CHR_IDX_DDL := replace(CHR_IDX_DDL,'"','');
 
                         -- DDL書き出し(インデックス)
                         UTL_FILE.PUT_LINE(TYP_FILE_HUNDLE,CHR_IDX_DDL);
                         UTL_FILE.NEW_LINE(TYP_FILE_HUNDLE,1);
                     end loop;
  
                     close CUR_TARGET_OBJ_INDEXES;
                 end if;

                 -- コメント数をチェック
                 select sum(cnt) into NMB_CHECK_COM
                 from (
                      select 'TAB_COMMENTS' as KEY ,count(1) cnt
                      from user_tab_comments
                      where table_name = REC_TARGET_TABLES.TAB_NAME
                      union all
                      select 'COL_COMMENTS' as KEY ,count(1) cnt
                      from user_col_comments
                      where table_name = REC_TARGET_TABLES.TAB_NAME
                 );
                 if NMB_CHECK_COM > 0 then
                     -- DDL取得
                     select DBMS_METADATA.GET_DEPENDENT_DDL('COMMENT',REC_TARGET_TABLES.TAB_NAME)
                     into CHR_COM_DDL
                     from dual;

                     CHR_COM_DDL := replace(CHR_COM_DDL,'"','');

                     -- DDL書き出し(コメント)
                     UTL_FILE.PUT_LINE(TYP_FILE_HUNDLE,CHR_COM_DDL);
                     UTL_FILE.NEW_LINE(TYP_FILE_HUNDLE,1);
                 end if;
 
                 UTL_FILE.FCLOSE(TYP_FILE_HUNDLE);
 
             end if;
 
      end loop;

      close CUR_TARGET_TABLES;

EXCEPTION WHEN OTHERS THEN
	UTL_FILE.FCLOSE_ALL;
	RAISE;

end;
/



↑のコードを貼りつけてsqlplus等のツールで実行するだけで良い。
パーティションテーブル、パーティションINDEXにも対応。
出力はテーブルもしくはマテビューごとに別のファイルに出力されて、
そのテーブルやマテビューにインデックスやコメントがあればそれも一緒に出力される。



以下、注釈とか。

■事前準備
このスクリプトではUTL_FILEパッケージを使ってDDLを出力するので、
事前にディレクトリオブジェクトが必要になる。
↑のコーディングではdeclare部の変数「CHR_FILE_PATH」にそのディレクトリオブジェクト名を定義している
(例のためそのディレクトリオブジェクト名は"REEEEEEEEEMAN_TEST_DIR")
当然ながらサーバ上でディレクトリオブジェクトが指すパス位置に実際にディレクトリが存在することが前提になる。
また、実行するOracleのユーザーに対するディレクトリオブジェクトへの書き込み権限付与と、
同様にサーバ上の実際のディレクトリに適切な権限付与をしておくことも事前準備として必要になる。

ディレクトリオブジェクトの作成例

create or replace directory REEEEEEEEEMAN_TEST_DIR as '/home/oracle/reeeeeeeeeman_test';

ディレクトリオブジェクトへの権限付与(sysdbaにて、このスクリプトを実行するOracleのユーザーがSCOTTである場合)

grant write on directory REEEEEEEEEMAN_TEST_DIR to SCOTT;

◆サーバ上のディレクトリへの権限付与

$ chmod -R 777 /home/oracle/reeeeeeeeeman_test


■対象テーブル(マテビュー)の指定について
DDL取得対象のオブジェクトはカーソル定義「CUR_TARGET_TABLES」に直書きするつくりになっている。
全テーブル取得するならここのfrom句を「USER_TABLES」に変更すれば良い。
特定の1テーブルに絞りたいなら「USER_TABLES」に加えてWHERE句を付け、
かつそのテーブル名部分をバインド変数化してしまうと汎用的になるだろう。

  cursor CUR_TARGET_TABLES is
   select table_name as tab_name
   from user_tables
   where table_name := :target_tab_name

↑みたいな。
ただしこの場合sqlplusで実行する際は事前に以下のようにバインド変数定義が必要になる。

SQL> variable target_tab_name nvarchar2(20);
SQL> execute :target_tab_name := 'TEST_TABLE';


■テーブルとマテビューとの切り分けについて
USER_TABLESにはマテビューも含まれるので、
カーソルから取得した後USER_MVIEWSと照合してテーブルかマテビューかの切り分けを行っている。
テーブルと判断されたら「DDL_TAB」マテビューと判断されたら「DDL_MVW」が、
出力後のファイル名の先頭にそれぞれ付与される形になる。

■その他実装面での工夫
DBMS_METADATA.GET_DDLDBMS_METADATA.GET_DEPENDENT_DDLは、
指定したオブジェクトが存在しない場合エラーになる。
たとえばインデックスが一本もついていないテーブルTEST_TABLE_NO_IDXに対して
DBMS_METADATA.GET_DDL('INDEX','TEST_TABLE_NO_IDX')を実行するとエラーになる。
同様にコメントがまったくついていないテーブルTEST_TABLE_NO_COMMENTに対して
DBMS_METADATA.GET_DEPENDENT_DDL('COMMENT','TEST_TABLE_NO_COMMENT')を実行するとエラーになる。
なので、インデックスの場合はUSER_INDEXES、コメントの場合はUSER_TAB_COMMENTSとUSER_COL_COMMENTSを参照し、
そのテーブルないしマテビューにインデックスやコメントが付与されているかを調べてからDDL取得に移る。

■心残り
DBMS_METADATA.GET_DDLで取得したCLOBにはダブクォ(")

が含まれる。
これが嫌だったので、除去するために無理矢理replaceを挟み込んでいる。
よって、物理名ならともかく、コメント等の論理名にダブクォを含む場合、これも除去されてファイルに出力される。
DBMS_METADATA.SET_TRANSFORM_PARAMあたりで指定できるのかといろいろ探したが見つからなかったので、
もういいやと思ってあきらめた。

■愚痴
他のサイトでも書かれてたけどUTL_FILEパッケージの公式の説明わかりづらいよな…