FileMakerをベースとしたWebアプリにおいて、CSVといった外部ファイルからのデータインポートが壁になりやすいということは前回紹介したとおり。今回はいよいよ実装方法について紹介していこう。

PHP側でのループでデータを登録する方法

まずは前回の(1)「PHP側でのループでデータを登録する」方法だ。ファイルアップロード処理とFileMakerテーブルへのインポートを別々のPHPにわけておこなう。

データベースのテーブルは次のとおり。

テーブル「fxphp_csv」。CSVファイル名を格納する

テーブル「fxphp_data」。CSVのデータを格納する

ファイルアップロード画面 - fm_new.php

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">

<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>FX.php CSVファイルのアップロード</title>

</head>

<body>

    <form action="./fm_file_upload.php" accept-charset="utf-8" enctype="multipart/form-data" method="post">
        <table border="1">
            <tr>
                <th>
                    CSVファイル
                </th>
                <td align="left">
                    <input name="csvFile" type="file">
                </td>
            </tr>
        </table>

        <p>
            <input type="submit" value="アップロード">
        </p>

        <p>
            <a href="./fm_list.php">アップロードをキャンセル</a>
        </p>

    </form>

</body>

</html>

ファイルアップロード処理 - fm_file_upload.php

<?php
// 外部ファイルにて SYSTEM_ATTACH_PATH を定義しています
// define('SYSTEM_ATTACH_PATH', './attached/'); // 画像の保存先

include_once('./fx/FX.php');
include_once('./fx/server_data.php');

if (UPLOAD_ERR_OK === $_FILES['csvFile']['error'])
{
    // アップロードされたファイルをリネームし、SYSTEM_ATTACH_PATH 以下に保存
    $uploadFile['csvFile']['filename'] =
        date('YmdHis') . '_'. $_FILES['csvFile']['name'];
    move_uploaded_file
    (
        $_FILES['csvFile']['tmp_name'],
        SYSTEM_ATTACH_PATH.
        $uploadFile['csvFile']['filename']
    );

    // FileMakerにファイル名を保存
    $data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
    $data->SetDBData($databaseFileName,'fxphp_csv', 1);
    $data->SetDBUserPass($webUN,$webPW);
    $data->SetCharacterEncoding('utf8');
    $data->SetDataParamsEncoding('utf8');
    if (!empty($uploadFile['csvFile']['filename']))
    {
        $data->AddDBParam('fc_fileName' , $uploadFile['csvFile']['filename']);
    }
    $dataSet = $data->FMNew();

    if ( 0 === (int)$dataSet['errorCode'] )
    {
        include_once('./fm_list.php');
    }
    else
    {
        // エラー処理..
        echo 'レコードの登録に失敗しました。';
    }

    }
    else
    {
        // エラー処理..
        echo 'ファイルのアップロードに失敗しました。';
    }

?>

ファイル一覧 - fm_list.php

<?php

include_once('./fx/FX.php');
include_once('./fx/server_data.php');

// 文字列エスケープ用関数
function h($string)
{
    return htmlspecialchars(trim($string), ENT_QUOTES, 'UTF-8');
}
// レコードIDの取得用関数
function getRecid($str)
{
    $tmp = explode('.',$str);
    return $tmp[0];
}

$data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
$data->SetDBData($databaseFileName,'fxphp_csv', 10);
$data->SetDBUserPass($webUN,$webPW);
$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');

$dataSet = $data->FMFind();
?>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>FX.php データ一覧</title>
</head>

<body>

    <table border="1">
        <thead>
            <tr>
                <th>
                    &nbsp;
                </th>
                <th>
                    ファイル名
                </th>
                <th>
                    アップロード日時
                </th>
            </tr>
        </thead>
        <tbody>
        <?php
        foreach ( $dataSet['data'] as $key => $value )
        {
        ?>
            <tr>
                <td>
                    <a href="./fm_import_csv.php?recid=<?php echo getRecId($key); ?>">インポート</a>
                </td>
                <td>
                    <?php echo h($value['fc_fileName'][0]); ?>
                </td>
                <td>
                    <?php echo h($value['fc_registTimeStamp'][0]); ?>
                </td>
            </tr>
        <?php
        }
        ?>
        </tbody>
    </table>

    <p><a href="./fm_new.php">CSVファイルのアップロード</a></p>

</body>

</html>

インポート処理 - fm_import_csv.php

<?php

// 外部ファイルにて SYSTEM_ATTACH_PATH を定義しています
// define('SYSTEM_ATTACH_PATH', './attached/'); // CSVの保存先

include_once('./fx/FX.php');
include_once('./fx/server_data.php');

// 文字列エスケープ用関数
function h($string)
{
    return htmlspecialchars(trim($string), ENT_QUOTES, 'UTF-8');
}

// ファイル名取得
$data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
$data->SetDBData($databaseFileName,'fxphp_csv', 1);
$data->SetDBUserPass($webUN,$webPW);
$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');
$data->SetRecordID($_GET['recid']);
$dataSet = $data->FMFind();
if ('0' !== (string)$dataSet['errorCode'])
{
    echo 'レコードの取得に失敗しました。FileMaker エラー番号: ' . $dataSet['errorCode'];
    exit;
}

if (file_exists(SYSTEM_ATTACH_PATH.$dataSet['data'][key($dataSet['data'])]['fc_fileName'][0]))
{

    echo 'データをインポートしています';
    $count['error'] = 0;
    $count['ok'] = 0;

    $handle = fopen(SYSTEM_ATTACH_PATH.$dataSet['data'][key($dataSet['data'])]['fc_fileName'][0], 'r');
    while (($csv = fgetcsv($handle)) !== FALSE)
    {
        $data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
        $data->SetDBData($databaseFileName,'fxphp_data');
        $data->SetDBUserPass($webUN,$webPW);
        $data->SetCharacterEncoding('utf8');
        $data->SetDataParamsEncoding('utf8');
        $data->AddDBParam('fd_field_1', $csv[0]);
        $data->AddDBParam('fd_field_2', $csv[1]);
        $data->AddDBParam('fd_field_3', $csv[2]);
        $data->AddDBParam('fd_field_4', $csv[3]);
        $data->AddDBParam('fd_field_5', $csv[4]);
        $data->AddDBParam('fd_field_6', $csv[5]);
        $data->AddDBParam('fd_field_7', $csv[6]);
        $data->AddDBParam('fd_field_8', $csv[7]);
        $data->AddDBParam('fd_field_9', $csv[8]);
        $dataSet = $data->FMNew();
        if ('0' !== (string)$dataSet['errorCode'])
        {
            $count['error']++;
        }
        else
        {
            $count['ok']++;
        }
        echo '.';
    }
    fclose($handle);

    echo '<p>';
    echo 'インポート処理が終了しました。成功: ' . number_format($count['ok']) . '、失敗: ' . number_format($count['error']);
    echo '</p>';
}
else
{
    echo 'ファイル「' . h($dataSet['data'][key($dataSet['data'])]['fc_fileName'][0]) . '」が見つかりません。';
}

?>

ファイルアップロードのロジック自体は、本連載の第12回『オブジェクトフィールドから脱却! よりよいパフォーマンスを出す実装とは』で紹介したものを使用している。アップロードされたファイル名はいったんFileMakerのテーブルで管理し、ファイル一覧でレコードをリスト表示する。

ファイル一覧画面。まだなにもファイルをアップロードしていないので、一覧にはなにも表示されない

CSVファイルをアップした。CSVファイルは、日本郵便の「郵便番号データダウンロード」にて公開されているデータをUTF-8にエンコードしたものを使用した

一覧画面の左列に表示する「インポート」リンクをクリックしてインポート処理がおこなわれる。

インポートボタンをクリック後の画面。PHPでインポート処理(ループで-new)がおこなわれている間、FileMaker側にレコードが次々と登録されていく

この手法だとファイルをアップロードしてからユーザが任意のタイミングでインポート処理を実施できる。「ファイルアップロードしてからすぐインポートしたい」という要望にも応えられるが、この方法はデータの登録を回数分繰り返しているだけなので処理完了までに時間がかかりやすい。実際に使用する場合は、処理中のタイムアウトやWebブラウザを閉じてもいいように、PHPのsystem()やexec()を活用しよう。

FileMaker Serverのスケジュールで「レコードのインポート」スクリプトを実行する方法

続いて、FileMaker Serverのスケジュール機能を使って「スクリプトを実行する」を利用する。定時に指定のテーブルに保存されたファイルを取得し「レコードのインポート」スクリプトステップを実行する方法だ。

FileMakerスクリプト例。テーブルに保存されているCSVファイルを取得し「レコードのインポート」スクリプトステップを使用してインポートを実施する。FileMaker ProとFileMaker Serverでは動作やパスの書き方が違うので、あらかじめヘルプに目を通しておこう

FileMakerスケジュール例。サンプル用に3分毎と短い間隔だが、実運用では処理時間を考えて多めにとっておいたほうが良いだろう

(1)の実装に使用した「fxphp_csv」テーブルに、インポートしたいCSV情報が格納されている。Webアプリケーション側ではファイルをアップロードするだけで、あとはFileMaker Serverスケジュールがスクリプトを起動し、勝手にCSVをインポートしてくれる。

FileMakerスクリプトが起動し、レコードの登録が完了した。環境にもよるが、3,000件程度のCSVなら1~2秒で処理が完了する

結果はスケジュールのほか、ログファイルからも確認できる

FileMakerビルトインの機能を使用する分、処理時間は(1)案より短時間で済む。もちろんサーバー公開に対応するスクリプトステップならレコードのインポート以外も動作するため、複雑な処理でも実装しやすい。即時性を求められないインポートの場合は、この実装方法が確実だ。

ちなみにFileMaker Serverのスケジュールはfmsadminコマンドで実行できる。インポート処理中の排他処理が実現できれば、(1)と(2)のいいところ取りな実装となる。WebサーバとFileMaker Serverが同一マシンにインストールされている場合は、ぜひチャレンジしてみてほしい。

CSVインポートまとめ - 場面にあった実装を

今回紹介した2案の実装方法はおたがいにメリットデメリットがあり「一概にどちらが優れているか」とは言えない。前回紹介したメリットデメリットも交えつつ、もう一回まとめてみよう。

(1)案のメリット

  • 任意のタイミングでインポート処理が実行できる
  • インポート前にCSVデータをPHP側で加工できる

(1)案のデメリット

  • 処理完了までに時間がかかる。FileMaker Serverにかかる負荷も大きい

(2)案のメリット

  • FileMakerビルトインの機能を使用するため、処理が(1)と比較すると非常に高速
  • インポート前後にCSVデータをFileMakerの機能内で加工できる

(2)案のデメリット

  • FileMaker Server 10かつ、スケジュールが設定・実行可能な環境限定
  • FileMakerのスクリプト引数に、Webアプリからの入力値を渡せない
  • 個別にファイルのインポートが実行できない。複雑な権限構成のシステムでは不向き

(1)案で実装した方が良い事例

  • インポート処理に即時性が要求される
  • あつかうCSVファイルのサイズが小さい
  • 特殊なCSV形式の場合(1行≠1レコードなど)
  • Webからの入力値をもとにインポート動作を切り替えたい

(2)案で実装した方が良い事例

  • インポート処理のタイミングがスケジュールでの定時実行でも良い
  • あつかうCSVファイルのサイズが大きい
  • CSVファイル形式の事前チェックが不要
  • インポート以外にFileMaker側での処理が必要

これらの実装方法は適材適所だ。まずは自分で試してみて「自分のやりたい事」にマッチしているかを確認してから実装してみよう。