Salesforce Bulk API 2.0 & 外部IDを使ったデータ移行をjsforceでおこなう

2022/09/17に公開

副業で既存システムからSalesforceへのデータ移行をおこなっており、 Bulk API 2.0 をNode.jsから利用したのでその備忘メモになります✍

https://developer.salesforce.com/docs/atlas.ja-jp.238.0.api_asynch.meta/api_asynch/asynch_api_intro.htm

Salesforce 側の設定

Salesforceには外部 IDという概念があり、データの移行元で発番したIDをベースにレコードの一意性の識別をおこなえます

https://help.salesforce.com/s/articleView?id=000325076&type=1

外部IDを利用するには、あらかじめSalesforceのオブジェクトマネージャにて、データ移行をおこなう対象のオブジェクトに対して設定が必要です

例としてリード Lead にレコード追加をおこなっていくものとします
URLとしては https://${アカウント名}.lightning.force.com/lightning/setup/ObjectManager/Lead/FieldsAndRelationships/view にて、カスタム項目を新規作成します
項目作成時に、 外部システムの一意のレコード識別子として設定する値の重複を許可しない にチェックをしておきます

今回は、項目名が K_RECORD_NUMBER__c という値であったと仮定します
__c はカスタム項目作成時に自動的に追加されるサフィックスだそうです

加えて、リードに対してイベントおよび行動 Event および活動 Activity を紐付けて移行をおこないたいとします
Salesforceに慣れていないと若干わかりづらい概念ですが、活動に外部IDを含めたカスタム項目を定義した上で、行動でリードとの関連を含むレコード値を設定すると適切に紐付けができる…ということのようです

https://note.com/koji_matae/n/n43c42db4abfb

続けて、活動に外部IDを設定します

注意としては、これはリード側で定義しているものとは異なり、活動(行動)を一意に識別するための値を後に設定します
今回は、項目名が K_CALL_RESULT__c であったと仮定します

また、行動側の設定についても確認しておきます
こちらはSalesforceのデフォルト項目ですので確認のみでOKです

行動にはデータ型が 参照関係 と表示されている項目があり、ここでオブジェクト間の関連を定義します
今回はリードとの関連を設定したいので、 Who という項目に対して値の設定をおこないます

上記画面では WhoId と出ているのですが、正式な項目名は項目の詳細画面の以下に表示される Who が正しい値のようです(よくわかってない)

移行データの作成

必要な項目の定義と確認ができたらデータを用意します
移行に際しては、データ移行対象のオブジェクト(今回は LeadEvent)ごとにCSVファイルを作成します

リードのサンプルデータを以下に示します
先程定義した K_RECORD_NUMBER__c に対して、移行元のシステムで採番したID値を設定します

lead.csv
"K_RECORD_NUMBER__c","Name"
"1","Aさん"
"2","Bさん"
"3","Cさん"

行動のサンプルデータを以下に示します

event.csv
"K_CALL_RESULT__c","Lead:Who.K_RECORD_NUMBER__c","Subject"
"10000","1","Aさんが電話をしました(1回目)"
"10001","1","Aさんが電話をしました(2回目)"
"10002","2","Bさんが電話をしました(1回目)"
"10003","3","Cさんが電話をしました(1回目)"
"10004","3","Cさんが電話をしました(2回目)"
"10005","3","Cさんが電話をしました(3回目)"

K_CALL_RESULT__c についてはリードと同様に、行動を一意に識別するための外部IDですが、 Lead:Who.K_RECORD_NUMBER__c は見慣れない形式になっています
これは、 多態的な項目でのリレーション をおこないたい場合の特殊な記法で、 ${親オブジェクトID}:${子項目ID}.${親項目ID} の書式で指定します
サンプルデータの例だと、 (Eventの)Who項目の関連を、LeadのK_RECORD_NUMBER__cとレコード値を突合して設定する 的な意味になるようです

https://developer.salesforce.com/docs/atlas.ja-jp.238.0.api_asynch.meta/api_asynch/relationship_fields_in_a_header_row__2_0.htm

今回は、2つのファイルが path-to-csv/ ディレクトリ配下に存在するものとします

移行スクリプト

jsforce というすばらしいOSSがあるのでこちらをありがたく使います
執筆時点では 2.0 ブランチで最新版を開発しており、今回使いたいBulk API 2.0への対応もそちらでおこなわれているため、 npm i jsforce@latest でインストールしてください(私は 2.0.0-beta.18 を使用しました)

https://github.com/jsforce/jsforce

また、これはオプションですが、ファイル操作を楽にするため fs-extraも活用します

移行スクリプトのサンプルを以下に示します

loadAndWaitForResults メソッドを使ってリードと行動を直列に登録していくことで、紐付けが適切に行われます
また、 operation プロパティに upsert を指定すると、 externalIdFieldName で指定した外部ID名で一意性判定をおこない、レコード追加および更新をおこなえます
デターに

ikou.js
const { Connection } = require("jsforce");
const { createReadStream } = require("fs-extra");
const { existsSync, writeJSON } = require("fs-extra");
const { join } = require("path");

const main = async () => {
  const targetDir = process.argv[2];

  if (!existsSync(targetDir)) {
    throw new Error(`not found: targetDir=${targetDir}`);
  }

  const conn = new Connection();

  console.log("login");
  await conn.login(
    process.env.SALESFORCE_USER,
    process.env.SALESFORCE_PASSWORD
  );

  const params = [
    ["Lead", "upsert", "lead.csv", "K_RECORD_NUMBER__c"],
    ["Event", "upsert", "event.csv", "K_CALL_RESULT__c"],
  ];

  for (const [object, operation, fileName, externalIdFieldName] of params) {
    const targetFile = join(targetDir, fileName);
    console.log(`import start ${object}: ${targetFile}`);

    const input = createReadStream(targetFile);

    const { successfulResults, failedResults, unprocessedRecords } =
      await conn.bulk2.loadAndWaitForResults({
        object,
        operation,
        input,
        externalIdFieldName,
        pollTimeout: 1800000,
      });
    await writeJSON(`${targetFile}.success.json`, successfulResults, {
      spaces: 2,
    });
    await writeJSON(`${targetFile}.failed.json`, failedResults, {
      spaces: 2,
    });
    await writeJSON(`${targetFile}.unprocess.json`, unprocessedRecords, {
      spaces: 2,
    });
    console.log(
      `result: success=${successfulResults.length}, failed=${failedResults.length}, unprocessed=${unprocessedRecords.length}`
    );
  }
  console.log("complete!");
};

main().catch((reason) => {
  console.error(reason);
  process.exitCode = 1;
});

環境変数に SALESFORCE_USER および SALESFORCE_PASSWORD を指定した上で、移行スクリプトを実行します

$ node ikou.js path-to-csv/
login
import start Lead: path-to-csv/lead.csv
result: success=3, failed=0, unprocessed=0
import start Event: path-to-csv/event.csv
result: success=6, failed=0, unprocessed=0
complete!

Web画面でレコードができていたらOKです

登録がうまくできない場合は、スクリプト中で出力している各種JSONファイルを確認してください
外部ID指定によるUpsertをおこなっているため、基本的に全ての登録がSuccessになるまでデータ修正 & スクリプト実行を繰り返せばよいものと思います🐕‍🦺

そんだけ😌

Discussion