本文へ移動

技術ガイド

SaaSアプリケーション向けCSVインポーターの作り方

解析、マッピング、テナントに関する権限、検証、書き込み、再試行、照合を解説するエンジニア向けガイドです。簡単なインポートで済む場合の進め方も明示します。

Mohamed Atef · ImportFlow
最終レビュー

ユーザーがインポート予定の内容を確認でき、バックエンドが受け入れた全レコードの処理結果を説明できる、最小限のフローを作ります。再利用可能な機能では通常、制限を設けたファイルの受け付け、解析、承認済みの列マッピング、検証と修正、確定した書き込み計画、永続的な結果記録が必要です。取り込み先に書き込む処理は、ブラウザーとは独立して認可と再試行時の動作を保証しなければなりません。

既知のファイルを一度だけ扱うなら、レビュー済みのスクリプトやSupabaseのTable Editorで十分な場合があります。顧客1社のデータを移行するために、インポーター製品を作る必要はありません。本ガイドでは、再利用可能な機能の複雑さが必要になる場面を示し、各境界でエンジニアが判断すべき具体的な事項を説明します。

対象範囲と前提条件. PostgreSQL/Supabaseへの顧客データ取り込みを対象としたアーキテクチャの指針です。SQLの意味論はPostgreSQL 17に準拠します。疑似コードは設計を示すもので、ImportFlow SDKではありません。

再利用可能な機能が必要か判断する

要件最初に取り組む方法機能を拡張する理由
社内データセットが1つで、スキーマが安定しており、技術担当者が操作する組み込みのCSVインポート、または小規模なレビュー済みスクリプト再現性や監査の要件がスクリプトで対応できる範囲を超える
フィールドの意味が異なる、顧客1社のデータ移行明示的なマッピング、隔離環境での事前検証、レビュー済みの実行手順顧客が変わっても同じ判断が繰り返し必要になる
エンジニアが立ち会わずに顧客がファイルをアップロードする修正機能と永続的な結果記録を備えたインポートフローブラウザーのセッションを再現せずに、サポートが失敗を解決する必要がある

社内用アップローダーなら、訓練を受けた担当者と管理されたファイル形式を前提にできます。顧客のオンボーディングでは、通常そうはいきません。顧客は、どのシートを正とするか、列挙型の値がなぜ拒否されたか、どのレコードがすでに存在するかを知らない場合があります。UIでは、顧客にスキーマの理解を求めずに、そうした判断を見える形にする必要があります。

既存のパーサーやオープンソースのインポートUIを使えば、実装の手間を減らせます。実際に扱うファイルのケースで動作を評価し、どの時点からアプリケーション側が責任を持つのかを確認してください。検証済みの行を渡すコールバックは有用な部品ですが、データベースのトランザクション、テナントに関する権限、復旧手順までは定義しません。

当面の作業がSupabaseでの管理者インポートなら、まず組み込みインポート機能の手順を使ってください。

インポート仕様を文書化する

1行が何を表すか、処理が挿入・更新・upsertのどれなのかを定義します。それぞれ仕様は異なります。更新では、同一レコードと判定する条件と、変更を許可する内容について、それぞれ承認が必要です。重複エラーが出なくなるというだけの理由で、汎用的なupsertを使うことは避けてください。

連絡先については、次のような簡潔な仕様を定められます。

仕様の例
Operation: insert new contacts
Target: one approved application table
Source fields: external_contact_ref, email, display_name
Tenant: supplied by an authorized import job
Primary key: generated by the database
External identity: unique within tenant + source system
Missing display name: NULL
Existing external identity: report conflict; do not overwrite
Accepted set: frozen after review; edits require revalidation

対応するファイルの条件、変換規則、スキーマのバージョン、エラー方針、完了の判断に必要な証拠も記録します。これはエンジニアリングのための文書です。設定言語やコンパイラーは必要ありません。

バージョンによって、プレビューと実際に実行する動作を結び付けます。確定後にマッピングや正規化の規則が変わったら、古いプレビューは無効にしてください。取り込み先のスキーマが変わった場合は、以前レビューした列が同じ意味を保っていると決めつけず、互換性を再評価します。

ファイルと解析の上限を設定する

どこで解析するかを選びます。ブラウザーで解析すれば、試しに確認するファイルをローカルに保ち、プレビューの応答性を高められます。サーバーで解析すれば、タブを閉じても継続するジョブに対応できます。どちらにもリソースの上限が必要です。ブラウザーで行った検証は、書き込み前に、正当性を保証する側で再実施しなければなりません。

バイト数、レコード数、列数、セルの長さ、処理時間、保持するエラー詳細に上限を設けます。XLSXでは、圧縮ファイルのサイズだけでは、展開後のサイズ、シート数、セル数を制限できません。必要に応じてストリーミング処理や別の実行先への処理の移管を行い、ファイルや処理の上限を超えた場合は、役立つメッセージを表示して停止します。

アップロードでは、アクセス元を認証し、保存キーをシステム側で生成し、認可されたジョブごとにファイルを分離します。ファイル名をパスとして信用してはいけません。途中で放棄されたアップロード、完了したジョブ、失敗したジョブ、バックアップについて、保持方針を明示してください。削除の仕組みを持たずに「一時保存」したファイルは、実質的には無期限に保存されます。

初期バージョンをCSVのみの対応にするのも妥当です。ワークブックに対応する場合は、ユーザーがシートとヘッダー行を選べるようにし、数式とキャッシュ値の扱いを文書化してください。表計算ファイルのマクロや外部リンクは実行せず、無視した内容をユーザーに伝えます。

値を保持して解析する

カンマや改行で単純に分割せず、保守されているパーサーを使います。RFC 4180は引用符で囲まれた区切り文字や改行を説明していますが、実際のエクスポートは文字エンコーディング、区切り文字、空欄の扱いが異なります。受け付ける形式を文書化し、実行前に検出した解釈を提示してください。

識別子は文字列のまま保持します。自動的な数値変換は、先頭のゼロを消したり、長いアカウント番号を丸めたりすることがあります。Papa Parseの動的な型変換、ヘッダー処理、エラー報告などのオプションは、APIリファレンスに記載されています。意図を持って選択してください。

テストデータ定義すべき期待動作
引用符で囲まれたセル内のカンマまたは改行余分な列やレコードを作らず、1つのフィールド/1つのレコードとして扱う
UTF-8 BOMと非ASCII文字を含む名前ヘッダーを認識し、名前を保持する
重複するヘッダー、または1つのフィールドにマッピングされた2つのヘッダー黙って上書きせず、競合を明示する
00042と20桁の外部参照文字列を正確に保持する
空のセル、引用符で囲まれた空のセル、欠落したフィールド区別するか、意図的に同一視するかを文書化する
解釈が曖昧な日付、または地域固有の小数表記推測せず、解釈を確認する
不正な引用符、または想定外のフィールド数入力データ上の位置を示し、情報量を制限した診断を出す

修正から書き込みまで、入力レコードの識別子を追跡します。物理的な行番号はパーサーのエラー箇所を探すのに役立ちますが、引用符で囲まれた複数行のレコードは、いくつもの行にまたがることがあります。結果から元のレコードをたどれるようにし、レコードと物理行が常に同じだと扱わないでください。

書き込み可能なフィールドだけにマッピングする

入力データから値を指定することが実際に許可されている取り込み先フィールドを提示します。候補の提案は入力の手間を省けますが、新たな書き込み権限を作り出してはいけません。必須フィールドの不足、競合するマッピング、除外する入力列について、判断を必須にします。

「Owner」という入力ヘッダーは、営業担当者、ワークスペース、旧レコードの作成者を意味する可能性があります。類似度スコアだけでなく、取り込み先フィールドの説明とサンプル値を表示してください。後からサポートが結果を説明できるよう、確定したマッピングをジョブとともに保持します。

サーバーでも、未知のキーを再度拒否します。任意のリクエストキーからではなく、承認済みの取り込み先フィールドからデータベースへの書き込みを組み立ててください。値はパラメーター化し、動的な識別子が避けられない箇所では、レビュー済みの識別子の許可リストを使います。ファイル名やヘッダーを実行可能なSQLにしてはいけません。

デフォルト値、IDENTITY、生成式は別々に分類します。通常のデフォルト値があるだけでは、フィールドは保護されません。実行可能な例は、CSVインポート時にデータベースが設定する値を参照してください。

正規化と業務上の判断を分ける

正規化は承認された解釈を適用する処理であり、その解釈を選ぶのは業務上の判断です。合意済みの識別子形式に従って前後の空白を除去する処理は機械的に行えても、「former client」を「inactive」に対応付けるには、業務上の判断権限が必要です。推定したタイムゾーン、列挙値の置換、重複時に採用するレコードは、承認されるまで変更案として扱います。

検証は段階ごとに行い、確実に判定できる最も早い段階で、有用なエラーを返します。

  1. ファイル構造:対応する文字エンコーディング、レコード、ヘッダー、上限。
  2. フィールドの値:必須条件、型、範囲、受け付ける形式。
  3. ファイル内の関係:重複や、繰り返し出現する参照の不整合。
  4. 業務ルール:許可するステータス、同一性の判定基準、権限。
  5. 取り込み先の確認:適用される制約、参照先の存在、実際の書き込み動作。

取り込み先への照会結果は、ある一時点の事実です。プレビューから実行までの間に、別のトランザクションが同じ一意値を挿入することがあります。最終判断は制約に委ね、制約違反を処理してください。問題のないプレビューが得られても、値が予約されるわけではありません。

エラーには、機密性の高い値をログに残さず、レコード、フィールド、規則、修正方法の候補を示します。同じ問題をまとめ、体系的な問題をユーザーが一度で修正できるようにしてください。レコードを除外する場合は、件数と理由を明示する必要があります。黙って行を捨てると、後から説明できない不一致が生じます。

書き込み前に予定される結果を提示する

有用なプレビューには、受け入れ件数と除外件数、必要な修正、承認済みの変換、対象テナント、操作を表示します。実際の取り込み先に対して実行した確認と、静的またはローカルな確認を明示してください。

書き込みを行わない検証は、そのとおりに説明します。実際に書き込んでからロールバックするトランザクションは、必ずしも副作用のないドライランではありません。シーケンスの採番や、外部で開始された処理の影響は元に戻らないことがあります。書き込み動作は、そのために用意した隔離環境で事前検証してください。

確定内容を、ファイル、マッピング、規則、受け入れるレコードの集合と結び付けて保持します。確定後にセルを編集した場合は、再検証した新たな案を作成しなければなりません。大きな入力では、必要に応じて修正用グリッドを仮想化します。その際も、キーボード操作、フィールドのラベル、仮想化された行の外側に表示する概要を維持してください。

テナントに関する権限と外部キーを解決する

ジョブを、認可されたテナントと操作者に結び付けます。テナント関連フィールドの値は、信頼できる書き込み処理から設定してください。ブラウザーのhidden入力も、依然としてブラウザー側で操作できる入力です。RLSを設計に組み込む場合は、実際の実行ロールとポリシーの組み合わせでテストします。service-roleでのアクセスは、その境界を変えます。

「Company 42」のような参照は、通常、旧システムの行を識別しています。承認済みの外部識別キーを、正しいテナントと入力元の名前空間の範囲内で解決してください。一致が0件の場合と複数件の場合は、異なる失敗です。最初の行を選ぶと曖昧さが隠れてしまいます。最終的な外部キー制約と、テナントの整合性を保つ制約は維持してください。

これらの境界は、テナントに関する権限と外部キーの解決の詳細ガイドで示しています。複数テーブルの取り込み順や循環参照には、明示的なリレーションの計画が必要です。列マッピングの機能で扱うものではありません。

書き込みと処理記録を同時にコミットする

小規模で範囲の限られたインポートでは、単一トランザクションが最も単純な結果モデルになることがよくあります。大きな処理では、測定したトランザクションの所要時間、メモリ、ロック、データベース負荷に基づいてバッチを選びます。チュートリアルからコピーしたバッチサイズは、容量計画にはなりません。

各ジョブと各バッチに、安定した識別子を付けます。同一内容の再試行では、記録済みの同じ結果を返し、同じ識別子で内容を変えたペイロードは拒否する必要があります。処理記録をその書き込みの証拠とするには、記録と取り込み先への書き込みを同一トランザクションでコミットしなければなりません。

PostgreSQLトランザクションのアルゴリズム例
BEGIN
  authorize access to this job
  lock the existing job row FOR UPDATE
  look up receipt for (job_id, batch_number)
  if receipt exists:
    require matching payload hash and contract version
    return recorded result without inserting again
  else:
    check job state and the next expected batch
    validate and insert the approved rows
    insert receipt with identity, hash and result
    advance the job's committed progress
COMMIT

これは設計の概略であり、実行可能なSQLではありません。ジョブ行のロックによって、そのジョブの競合する実行を直列化し、処理記録の一意キーでデータベース制約を追加します。既存の処理記録を読むときにも認可を適用します。受け入れた値と、関係する仕様の識別情報を含む、安定したハッシュ用表現を定義してください。

処理記録を一方のデータベースに、取り込み先への書き込みを別のデータベースに置いて、その組み合わせをアトミックと呼んではいけません。システムをまたぐ必要があるなら、その間の不整合を解決する照合プロトコルを定めます。同様に、キューへの確認応答とデータベースのコミットも、同じ関数内で起きるというだけでは1つのトランザクションにはなりません。

コミット後に通知やWebhookが必要なら、同一トランザクションで書き込むoutboxに、未配信の処理を記録できます。ただし、配信には独自の冪等性と再試行の規則が引き続き必要です。通常のイベントペイロードには、インポートした行の内容を含めないでください。

部分的な結果と不明な結果を正確に表す

応答が失われても、トランザクションが失敗した証拠にはなりません。永続的な処理記録を照会するか、冪等な経路を通じて同一のバッチを再試行してください。その証拠にアクセスできない場合は、結果が未確定であることを表示し、無条件での全件再試行を止めます。

バッチ1〜4がコミットし、バッチ5が失敗したなら、コミット済みの部分集合と、失敗または未実行の残りを報告します。キャンセルボタンは今後の処理を止められても、コミット済みバッチを元に戻すとは約束できません。取り消し操作には、参照やその後の編集への影響も含め、別途レビューした補償変更が必要です。

行単位で失敗を許容する動作を明示します。「無効な行をスキップする」とは、顧客が部分的なインポートを受け入れ、スキップしたレコードを特定できるという意味です。バッチを全件成功か全件失敗にする方法も妥当な選択肢であり、特に業務オブジェクトが一部だけ作成されると誤解を招く場合に適しています。どちらを選んでも、汎用的な成功メッセージで隠してはいけません。

リクエストのタイムアウト、ブラウザーの切断、処理時間の長さによって同期実行が適さなくなったら、バックグラウンドジョブが役立ちます。再開に必要な状態を永続化し、再試行の回数などを制限し、停止したままの処理を調査または停止する手段を用意します。キューを使うだけでは、冪等性は自動的に得られません。

完了条件とサポート方法を定める

安定したキーを使って、受け入れた集合とコミット済みレコードを照合し、重要なフィールドと業務上の不変条件を比較します。件数は最初の確認です。同じ件数でも、異なるレコードを含む集合は検出できません。検証ガイドでは、この失敗をSQLで示しています。

ジョブ識別子、バージョン、件数、エラーの分類、所要時間、永続化した状態遷移をログに記録します。データベースのエラーが別テナントのデータを露出し得る場合は、表示前に機密部分を伏せてください。診断資料へのアクセスを制限し、削除すべき時期も記録します。

タブを閉じた後でも役立つ結果ページを用意します。何を受け入れ、何がコミットされ、何が未解決で、担当者が次に何をできるかを表示してください。エクスポートする修正用ファイルにも、表計算の数式への対策が必要です。CSV構文として引用符を付けるだけでは、表計算ソフトが危険なセルを数式として解釈することは防げません。

リリース前に、コミット応答の消失、同一バッチの同時送信、古い再試行キーで内容を変えたペイロードの送信、テナント権限の取り消し、プレビュー後のスキーマ変更、コミット済みバッチを持つジョブのキャンセルを検証します。こうしたケースからは、大きくても問題のないデモファイルより、インポーターについて多くのことが分かります。

出典と関連資料