【PL/SQL】UTL_FILEでCSV・ログファイルを読み書きする方法|DIRECTORY設定とエラー対処

【PL/SQL】外部ファイルを読み書きする方法(UTL_FILEの基本と注意点) PL/SQL

UTL_FILE は、Oracle Databaseサーバ上のファイルをPL/SQLから読み書きするための標準パッケージです。CSV出力、バッチログ、連携ファイルの読み込み、障害調査用の一時ファイル出力など、業務バッチでよく使われます。

この記事では、UTL_FILE の基本仕様をなぞるだけでなく、PL/SQL側でよく使う「CSVを書き出す」「CSVを1行ずつ読む」「ログを追記する」「例外時に必ず閉じる」「ファイル名を検証する」という実務レシピを中心に整理します。網羅的な仕様確認は Oracle UTL_FILE完全ガイド、大量データのジョブ化は DBMS_SCHEDULER完全ガイド もあわせて確認してください。

この記事で扱うこと

  • CREATE DIRECTORY と権限設定
  • FOPENPUT_LINEGET_LINEFCLOSE の基本
  • CSV出力、CSV読み込み、ログ追記の実装例
  • FFLUSHFCLOSE_ALLFOPEN_NCHAR の使いどころ
  • ORA-29283INVALID_PATHNO_DATA_FOUND の切り分け
  • 外部表、DBMS_LOBDBMS_OUTPUT との使い分け
スポンサーリンク

UTL_FILEを使う前提

UTL_FILE が扱うファイルは、クライアントPCではなくデータベースサーバ上のファイルです。SQL Developerやアプリケーションサーバのローカルファイルを直接読むわけではありません。そのため、OS上のディレクトリ権限、Oracleのディレクトリオブジェクト、データベースユーザーへの権限付与をセットで確認します。

場所DBサーバ上のディレクトリを読み書きします。
権限OS権限とOracleの DIRECTORY 権限の両方が必要です。
パスPL/SQLからはディレクトリ名で指定し、任意の絶対パスを直接渡しません。
上限max_linesize は最大 32767 です。長大行やLOBは別設計を検討します。

DIRECTORYを作成して権限を付与する

現在のOracleでは、古い UTL_FILE_DIR 初期化パラメータではなく、ディレクトリオブジェクトを使うのが基本です。ディレクトリオブジェクトはDBAまたは必要権限を持つユーザーが作成し、実行ユーザーへ READ / WRITE を付与します。

directory-setup.sql
-- DBAまたは権限を持つユーザーで実行
CREATE OR REPLACE DIRECTORY APP_EXPORT_DIR AS '/u01/app/export';

GRANT READ, WRITE ON DIRECTORY APP_EXPORT_DIR TO APP_USER;

-- 必要に応じて確認
SELECT directory_name, directory_path
FROM all_directories
WHERE directory_name = 'APP_EXPORT_DIR';

Oracle側の権限だけでなく、OSユーザーがそのディレクトリへ読み書きできる必要があります。ORA-29283: invalid file operation が出る場合は、ディレクトリ名の誤り、OSパスの存在、OS権限、ファイル名、モード、改行や文字コードを順に確認します。

CSVを書き出す基本形

CSV出力では、FOPEN でファイルを開き、PUT_LINE で1行ずつ書き、最後に FCLOSE で閉じます。例外が発生してもハンドルを閉じるように、IS_OPEN を使って後始末します。

write-csv.sql
DECLARE
  l_file UTL_FILE.FILE_TYPE;
BEGIN
  l_file := UTL_FILE.FOPEN(
              location     => 'APP_EXPORT_DIR',
              filename     => 'orders.csv',
              open_mode    => 'w',
              max_linesize => 32767
            );

  UTL_FILE.PUT_LINE(l_file, 'ORDER_ID,CUSTOMER_ID,AMOUNT');

  FOR r IN (
    SELECT order_id, customer_id, amount
    FROM orders
    ORDER BY order_id
  ) LOOP
    UTL_FILE.PUT_LINE(
      l_file,
      r.order_id || ',' ||
      r.customer_id || ',' ||
      r.amount
    );
  END LOOP;

  UTL_FILE.FCLOSE(l_file);
EXCEPTION
  WHEN OTHERS THEN
    IF UTL_FILE.IS_OPEN(l_file) THEN
      UTL_FILE.FCLOSE(l_file);
    END IF;
    RAISE;
END;
/

CSVの値にカンマ、ダブルクォート、改行が含まれる可能性がある場合は、単純連結では壊れます。実務ではCSVエスケープ関数を用意し、文字列列は必ずダブルクォートで囲む設計にすると安全です。

CSVエスケープ関数を用意する

CSVは見た目よりも事故りやすい形式です。顧客名にカンマが入る、備考に改行が入る、ダブルクォートが入る、といったケースを想定しておきます。

csv-escape-function.sql
CREATE OR REPLACE FUNCTION csv_value(p_value IN VARCHAR2)
RETURN VARCHAR2
AS
BEGIN
  IF p_value IS NULL THEN
    RETURN '""';
  END IF;

  RETURN '"' || REPLACE(p_value, '"', '""') || '"';
END;
/

-- 使用例
SELECT csv_value('A,B "test"') AS escaped_value
FROM dual;

数値や日付も、環境依存の書式で出力すると取り込み側で失敗します。日付は TO_CHAR(order_date, 'YYYY-MM-DD HH24:MI:SS') のように明示し、数値も小数点や桁区切りのNLS差に注意します。

CSVを1行ずつ読み込む

ファイル読み込みでは、GET_LINE をループで呼び出します。ファイル終端に到達すると NO_DATA_FOUND が発生するため、これは異常ではなく正常終了として扱います。

read-csv.sql
DECLARE
  l_file UTL_FILE.FILE_TYPE;
  l_line VARCHAR2(32767);
BEGIN
  l_file := UTL_FILE.FOPEN(
              location     => 'APP_EXPORT_DIR',
              filename     => 'orders.csv',
              open_mode    => 'r',
              max_linesize => 32767
            );

  LOOP
    BEGIN
      UTL_FILE.GET_LINE(l_file, l_line);

      -- ここでCSVを解析してテーブルへ取り込む
      DBMS_OUTPUT.PUT_LINE(l_line);
    EXCEPTION
      WHEN NO_DATA_FOUND THEN
        EXIT;
    END;
  END LOOP;

  UTL_FILE.FCLOSE(l_file);
EXCEPTION
  WHEN OTHERS THEN
    IF UTL_FILE.IS_OPEN(l_file) THEN
      UTL_FILE.FCLOSE(l_file);
    END IF;
    RAISE;
END;
/

NO_DATA_FOUND は通常の SELECT INTO でも使われる例外です。意味を取り違えやすい場合は ORA-01403 完全ガイド も確認してください。デバッグ出力は DBMS_OUTPUTの使い方 が参考になります。

ログファイルへ追記する

バッチの軽い実行ログをファイルへ追記したい場合は、open_mode => 'a' を使います。ただし、監査性や検索性が必要なログはファイルではなくログテーブルへ残す方が扱いやすいです。

append-log.sql
CREATE OR REPLACE PROCEDURE write_batch_log(
  p_message IN VARCHAR2
) AS
  l_file UTL_FILE.FILE_TYPE;
BEGIN
  l_file := UTL_FILE.FOPEN(
              location     => 'APP_EXPORT_DIR',
              filename     => 'batch.log',
              open_mode    => 'a',
              max_linesize => 32767
            );

  UTL_FILE.PUT_LINE(
    l_file,
    TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD HH24:MI:SS.FF3') ||
    ' ' || p_message
  );

  UTL_FILE.FFLUSH(l_file);
  UTL_FILE.FCLOSE(l_file);
EXCEPTION
  WHEN OTHERS THEN
    IF UTL_FILE.IS_OPEN(l_file) THEN
      UTL_FILE.FCLOSE(l_file);
    END IF;
    RAISE;
END;
/

FFLUSH はバッファ内容をファイルへ反映したい場面で使います。長時間処理の途中経過を外部から見たい場合には役立ちますが、頻繁に呼びすぎるとI/O負荷が増えるため、行数や処理単位で調整します。

ファイル名を検証する

UTL_FILE ではディレクトリオブジェクトで場所を限定できますが、ファイル名を外部入力から受け取る場合は検証が必要です。パス区切り文字、相対パス、制御文字、想定外の拡張子を拒否し、できればサーバ側で決めたファイル名だけを使います。

safe-filename.sql
CREATE OR REPLACE FUNCTION safe_export_filename(
  p_filename IN VARCHAR2
) RETURN VARCHAR2
AS
BEGIN
  IF p_filename IS NULL THEN
    RAISE_APPLICATION_ERROR(-20001, 'filename is required');
  END IF;

  IF NOT REGEXP_LIKE(p_filename, '^[A-Za-z0-9_-]+\.csv$') THEN
    RAISE_APPLICATION_ERROR(-20002, 'invalid filename');
  END IF;

  RETURN p_filename;
END;
/

ファイル名に ../ やサブディレクトリを渡して任意ファイルを触れるような設計は避けます。ファイル連携では「置き場所はDIRECTORYで固定」「名前は許可パターンで固定」「拡張子も固定」を基本にします。

NCHAR系ファイルを扱う場合

文字コードや国際化文字セットを意識する場合、FOPEN_NCHARGET_LINE_NCHARPUT_LINE_NCHAR も候補になります。ただし、相手システムが期待する文字コード、BOMの有無、改行コードを確認しないと、ファイルは作れても取り込み側で文字化けします。

nchar-file.sql
DECLARE
  l_file UTL_FILE.FILE_TYPE;
BEGIN
  l_file := UTL_FILE.FOPEN_NCHAR(
              location     => 'APP_EXPORT_DIR',
              filename     => 'message.txt',
              open_mode    => 'w',
              max_linesize => 32767
            );

  UTL_FILE.PUT_LINE_NCHAR(l_file, N'日本語メッセージ');
  UTL_FILE.FCLOSE(l_file);
END;
/

例外処理とFCLOSE_ALL

ファイル処理では、例外時にファイルハンドルを閉じることが重要です。通常は個別に FCLOSE しますが、複数ファイルを扱う複雑な処理では、最後の保険として FCLOSE_ALL を使うこともあります。

close-all.sql
DECLARE
  l_in_file  UTL_FILE.FILE_TYPE;
  l_out_file UTL_FILE.FILE_TYPE;
BEGIN
  l_in_file := UTL_FILE.FOPEN('APP_EXPORT_DIR', 'input.csv', 'r', 32767);
  l_out_file := UTL_FILE.FOPEN('APP_EXPORT_DIR', 'output.csv', 'w', 32767);

  -- file processing
  NULL;

  UTL_FILE.FCLOSE(l_in_file);
  UTL_FILE.FCLOSE(l_out_file);
EXCEPTION
  WHEN OTHERS THEN
    UTL_FILE.FCLOSE_ALL;
    RAISE;
END;
/

FCLOSE_ALL は便利ですが、乱用するとどのファイルをどこで閉じたのか追いづらくなります。基本はハンドルごとに閉じ、最後の安全網として使うくらいが扱いやすいです。

よくあるエラーと確認ポイント

ORA-29283パス、OS権限、ディレクトリ名、ファイル名、モードを確認します。
INVALID_PATHDIRECTORY 名が違う、権限がない、OSパスが存在しない可能性があります。
INVALID_MODErwa などのモード指定を確認します。
INVALID_FILEHANDLE開いていないハンドル、すでに閉じたハンドルを使っていないか確認します。
NO_DATA_FOUNDGET_LINE のEOFなら正常終了として扱います。
VALUE_ERROR行が max_linesize を超えていないか、受け取り変数が短くないか確認します。
WRITE_ERRORディスク不足、OS権限、ファイルシステム障害、同時書き込みを確認します。

大量CSVは外部表も検討する

UTL_FILE で1行ずつCSVを読む方法は柔軟ですが、大量データの取り込みには向かないことがあります。大量CSVをSQLで検索・検証・一括ロードしたい場合は、外部表やSQL*Loaderなどの利用も検討します。

UTL_FILE行ごとに独自チェックや個別ログを出したい処理に向きます。
外部表CSVを表のようにSELECTし、SQLで一括検証したい処理に向きます。
SQL*Loader大量ロードや運用ツールとしての取り込みに向きます。
DBMS_LOB大きなCLOB/BLOBを扱う場合はLOB用APIを検討します。

大きなファイルやLOBを扱う場合は DBMS_LOB完全ガイド、CLOB/BLOB変換時の ORA-22835ORA-22835の原因と解決方法 が参考になります。

ジョブ連携時の運用ポイント

CSV出力やファイル取り込みは、手動実行ではなくジョブとして動かすことが多いです。ジョブ化する場合は、ファイル名の日時付与、二重起動防止、処理済みファイルの退避、失敗時の再実行、ログ保存期間を決めておきます。

scheduled-export.sql
CREATE OR REPLACE PROCEDURE export_orders_daily AS
  l_filename VARCHAR2(128);
BEGIN
  l_filename := 'orders_' || TO_CHAR(SYSDATE, 'YYYYMMDD') || '.csv';
  l_filename := safe_export_filename(l_filename);

  -- 実際のCSV出力処理を呼び出す
  -- write_orders_csv(l_filename);

  INSERT INTO batch_log(process_name, status, message, created_at)
  VALUES ('export_orders_daily', 'SUCCESS', l_filename, SYSDATE);
EXCEPTION
  WHEN OTHERS THEN
    INSERT INTO batch_log(process_name, status, message, created_at)
    VALUES ('export_orders_daily', 'ERROR', SQLERRM, SYSDATE);
    RAISE;
END;
/

ジョブ履歴を集めて監視する設計は ジョブ実行履歴の集中管理と異常検知 が参考になります。簡易的な動作確認では DBMS_OUTPUT も使えますが、本番運用ログはテーブルへ残す方が検索・集計しやすいです。

本番導入前チェックリスト

DIRECTORYDB上のディレクトリ名とOSパスが正しいか。
OS権限Oracle実行ユーザーが読み書きできるか。
DB権限実行スキーマに READ / WRITE が付与されているか。
ファイル名外部入力をそのまま使わず、許可パターンで検証しているか。
CSVカンマ、ダブルクォート、改行、NULL、日付、数値書式を考慮しているか。
例外処理例外時も FCLOSE できるか。EOFの NO_DATA_FOUND を正常終了として扱えるか。
性能大量データなら外部表やSQL*Loaderを検討したか。
運用処理済みファイル退避、再実行、ログ、保存期間、容量監視を決めたか。

まとめ

UTL_FILE は、PL/SQLからCSVやログファイルを読み書きする便利なパッケージです。実務では、ディレクトリオブジェクトと権限設定、FOPEN / GET_LINE / PUT_LINE / FCLOSE の基本、例外時の後始末、ファイル名検証をセットで考える必要があります。

小さなファイル連携やログ出力なら UTL_FILE が扱いやすい一方、大量CSVは外部表、LOBは DBMS_LOB、定期実行は DBMS_SCHEDULER と組み合わせると安定します。ファイル処理はOS・DB・業務運用の境界にあるため、本番前のチェックを丁寧に行いましょう。