開発情報・ナレッジ

投稿者: ShiningStar株式会社 2025年9月4日 (木)

CSVファイルをページ上から一括更新するサンプルプログラム

ブラウザ上でCSVファイルを解析し、SPIRALのデータベースに一括更新するサンプルプログラムを紹介します。

注意点

このサンプルは、SPIRALのページで動作することを前提としています。
更新先のDBと、CSVの列構成を事前に一致させておく必要があります。
CSVのヘッダー行にはフィールドの識別名を入れる必要があります。
$keyName
には更新キーとなるフィールドの識別名及びヘッダーのカラム名と合致した物を入力してください。

実装の概要

1. ユーザーがファイル選択フォームでCSVファイルを選ぶ。
2. JavaScriptがファイルを読み込み、ヘッダー行とデータ行に分解してJSON文字列に変換する。
3. 変換したJSONを、フォーム内のhiddenフィールドにセットする。
4. ユーザーが「アップロード」ボタンを押すと、フォームが同じページにPOST送信される。
5. サーバーサイドのPHPがPOSTされたJSONデータを受け取る。
6.
$commonBase->apiCurlAction("PATCH", "/apps/" . APP_ID . "/dbs/" . DB_ID . "/records/bulk", $payload);

を使用して、データをDBに一括更新する。
7. 処理結果のメッセージを画面に表示する。

HTMLコード (bodyタブ)

まず、ユーザーが操作する画面を作成します。

<!-- 更新結果メッセージの表示 -->
<p th:if="${cp.result.value['result'] != null}" th:text="${cp.result.value['result']}"></p>

<form id="csvForm" method="POST">
  <input type="file" id="csvFile" accept=".csv" required>
  <input type="hidden" name="columns_json" id="columnsJson">
  <input type="hidden" name="data_json" id="dataJson">
  <button type="submit">CSVをアップロード</button>
</form>
            

JavaScriptコード (JavaScriptタブ)

次に、クライアントサイドの動作を担当するJavaScriptです。
CSVファイルを解析し、フォーム送信用のデータを作成します。

document.addEventListener('DOMContentLoaded', function () {
    function parseCSV(text) {
      const rows = [];
      let row = [];
      let value = '';
      let inQuotes = false;
      let i = 0;
  
      while (i < text.length) {
        const char = text[i];
        const next = text[i + 1];
  
        if (inQuotes) {
          if (char === '"' && next === '"') {
            value += '"';
            i++;
          } else if (char === '"') {
            inQuotes = false;
          } else {
            value += char;
          }
        } else {
          if (char === '"') {
            inQuotes = true;
          } else if (char === ',') {
            row.push(value);
            value = '';
          } else if (char === '\n' || char === '\r') {
            if (char === '\r' && next === '\n') i++;
            row.push(value);
            value = '';
            rows.push(row);
            row = [];
          } else {
            value += char;
          }
        }
        i++;
      }
  
      if (value !== '' || row.length > 0) {
        row.push(value);
        rows.push(row);
      }
  
      return rows;
    }
  
    document.getElementById('csvFile').addEventListener('change', function () {
      const file = this.files[0];
      if (!file) return;
  
      const reader = new FileReader();
      reader.onload = function (e) {
        const csvText = e.target.result;
        const parsed = parseCSV(csvText);
        if (parsed.length < 2) {
          alert('データ行が不足しています');
          return;
        }
  
        const columns = parsed[0];
        const data = parsed.slice(1).filter(row => row.length > 0 && row.some(v => v !== ''));
  
        document.getElementById('columnsJson').value = JSON.stringify(columns);
        document.getElementById('dataJson').value = JSON.stringify(data);
      };
      reader.readAsText(file, 'UTF-8');
    });
  });
            

PHPコード (PHPタブ)

最後に、サーバーサイドでAPI連携を行うPHPスクリプトです。
フォームからPOSTされたJSONデータを受け取り、SPIRAL APIを呼び出してDB更新処理を実行します。

<?php
//------------------------------
// 設定値
//------------------------------
define("API_URL", "https://api.spiral-platform.com/v1");
define("API_KEY", "");
define("APP_ROLE", "");
define("APP_ID", "");
define("DB_ID", "");

// ▼ 任意の更新キー名を指定(例:'id' ならレコードID、'email' などフィールド識別名)
$keyName = 'csvId'; // ←必要に応じて変更('_id' としてもOK)

$columns = $SPIRAL->getParams("columns_json");
$data    = $SPIRAL->getParams("data_json");

// POST受信処理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($columns, $data) && $columns && $data) {

    if (isset($columns[0]) && is_string($columns[0]) && str_starts_with($columns[0], '[')) {
        $columns = json_decode($columns[0], true);
    }
    if (isset($data[0]) && is_string($data[0]) && str_starts_with($data[0], '[')) {
        $data = json_decode($data[0], true);
    }

    if (!is_array($columns) || !is_array($data)) {
        $message = "データ形式が不正です";
        $SPIRAL->setTHValue("result", $message);
    }

    // キー列の位置を特定
    // 'id' or '_id' はID列として扱う。それ以外は任意フィールド名として扱う
    $isRecordIdKey = ($keyName === 'id' || $keyName === '_id');

    // columns に keyName が含まれていないと更新対象を特定できない
    $searchKey = $isRecordIdKey ? ($keyName === '_id' ? 'id' : 'id') : $keyName;
    $keyIndex  = array_search($searchKey, $columns, true);
    if ($keyIndex === false) {
        $message = "更新キー '{$keyName}' に対応する列が columns に見つかりません";
        $SPIRAL->setTHValue("result", $message);
    }

    $commonBase = CommonBase::getInstance();
    $records = [];

    foreach ($data as $row) {
        if (!is_array($row)) continue;
        // まず更新キーを取り出し
        if (!array_key_exists($keyIndex, $row) || $row[$keyIndex] === '' || $row[$keyIndex] === null) {
            // 更新キー未指定の行はスキップ
            continue;
        }
        $keyValue = $row[$keyIndex];

        // レコード本体を生成(列順でマッピング)
        $rec = [];
        foreach ($columns as $idx => $colName) {
            if (!array_key_exists($idx, $row)) continue;
            $value = $row[$idx];

            // APIではIDは _id フィールド名、任意フィールドはそのまま
            if ($isRecordIdKey && $colName === 'id') {
                // 値は後で _id に入れるのでスキップ(必要なら通常フィールドとして残しても良いが冗長)
                continue;
            }
            // 更新不可フィールド(password/file/user)はサーバ側でエラーになるため送らない方が安全
            $rec[$colName] = $value;
        }

        // 更新キーの付与
        if ($isRecordIdKey) {
            $rec['_id'] = $keyValue;
        } else {
            // 任意の「入力必須かつ重複不可」のテキスト/メールアドレス型フィールド名
            $rec['_updateKey'] = [$keyName => $keyValue];
        }

        $records[] = $rec;
    }

    if (empty($records)) {
        $message = "更新対象がありません({$keyName} の値が空または行が不正)";
        $SPIRAL->setTHValue("result", $message);
    }

    // PATCH /records/bulk による一括更新
    $payload = ["records" => $records];
    $result  = $commonBase->apiCurlAction("PATCH", "/apps/" . APP_ID . "/dbs/" . DB_ID . "/records/bulk", $payload);

    // 成功/失敗判定(環境差に配慮)
    if (is_array($result)) {
        if ((isset($result["ids"]) && is_array($result["ids"]) && count($result["ids"]) > 0)
            || (isset($result["updated"]) && (int)$result["updated"] > 0)
            || (isset($result["count"]) && (int)$result["count"] > 0)) {

            $idsText =
                isset($result["ids"]) && is_array($result["ids"]) ? ("(ID: " . implode(", ", $result["ids"]) . ")") : "";
            $count =
                isset($result["updated"]) ? (int)$result["updated"] :
                (isset($result["count"]) ? (int)$result["count"] : count($records));

            $message = "一括更新に成功しました:{$count}件 {$idsText}";
        } else {
            $message = "更新エラー:" . json_encode($result, JSON_UNESCAPED_UNICODE);
        }
    } else {
        $message = "更新エラー:API応答の解析に失敗しました";
    }

    $SPIRAL->setTHValue("result", $message);

} else {
    $SPIRAL->setTHValue("result", "");
}

//------------------------------
// 共通モジュール
//------------------------------
class CommonBase {
    /**
     * シングルトンインスタンス
     * @var UserManager
     */
    protected static $singleton;

    public function __construct() {
        if (self::$singleton) {
            throw new Exception('must be singleton');
        }
        self::$singleton = $this;
    }
    /**
     * シングルトンインスタンスを返す
     * @return UserManager
     */
    public static function getInstance() {
        if (!self::$singleton) {
            return new CommonBase();
        } else {
            return self::$singleton;
        }
    }
    /**
     * V2用 API送信ロジック
     * @return Result
     */
    function apiCurlAction($method, $addUrlPass, $data = null, $multiPart = null, $jsonDecode = null) {
        $header = array(
            "Authorization:Bearer ". API_KEY,
            "X-Spiral-Api-Version: 1.1",
        );
        if($multiPart) {
            $header = array_merge($header, array($multiPart));
        } else {
            $header = array_merge($header, array("Content-Type:application/json"));
        }
        if(APP_ROLE){
			$header = array_merge($header, array("X-Spiral-App-Role: ".APP_ROLE));
		}
        // curl
        $curl = curl_init();
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_URL, API_URL. $addUrlPass);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
        if ($method == "POST") {
            if ($multiPart) {
                curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
            } else {
                curl_setopt($curl, CURLOPT_POSTFIELDS , json_encode($data));
            }
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        if ($method == "PATCH") {
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        if ($method == "DELETE") {
            curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);
        }
        $response = curl_exec($curl);
        if (curl_errno($curl)) echo curl_error($curl);
        curl_close($curl);
        if($jsonDecode){
			return $response;
		}else{
            return json_decode($response, true);
		}
    }
}
?>
            

実行結果

ページにアクセスすると、ファイル選択フォームが表示されます。
CSVファイルを選択して「アップロード」ボタンをクリックすると、
ページがリロードされ、処理結果(成功またはエラーメッセージ)が画面上部に表示されます。

まとめ

本記事では、JavaScriptでCSVを解析し、PHPでSPIRALのDBに更新する一連の流れを解説しました。
実際に活用される際はCSVファイルの中身に応じて適切なパースロジックを実装する必要があります。
解決しない場合はこちら コンテンツに関しての
要望はこちら