☁️

[Salesforce] レコードをSandboxから本番にリリースする

2023/08/31に公開

株式会社TERASSでSalesforceをやったりやらなかったりしているimslpです。

Salesforceでリリースをする時レコードは含められません。しかし、レコードをマスタとして使うことはわりとあるケースだと思います。
レコードの更新をするためにインポートデータを用意するのも時間がかかるし、手動でやったらミスが起こるので大変です。そういうときにレコードをSandboxから本番にコマンド一発でリリースできたら非常に便利です。
標準機能だとSalesforce to Salesforceが思い浮かびますが、いくつか制限がありそうなので今回は自作します。それにこういうのは自分で作ったほうが楽しいです。

というわけで、今回はjsforceを使ってレコードを組織to組織でリリースするスクリプトを作っていきます。
https://github.com/jsforce/jsforce
https://jsforce.github.io/

前提

SalesforceやTypescript、Nodeなどの基礎情報ついては取り扱いません。
また、どのような形でスクリプトを実行するかはおまかせしますが今回はyargsを使ったCLIコマンドで実行します。
yargsや実行方法、組織との接続方法などについても省略します。送信元と送信先の2つのConnectionを受け取れる関数があればOKです。

環境

  • Node: 16
  • jsforce: 2.0.0-beta.19

処理の流れ

  1. 送信元から親レコードを取得
  2. 送信先に親レコードをUpsert
  3. 送信元から子レコードを取得
  4. 親レコードのUpsert結果を元に送信先に子レコードをUpsert

これだけです。単純です。
親子関係が無い単純なオブジェクトを更新するだけなら何も考えなくて良いですが、親子関係がある場合は少しだけ工夫する必要があります。順番にコードを見ていきます。

送信元から親レコードを取得

送信元から親オブジェクトのレコードを取得しています。ここで取得した項目でそのままUpsertするので更新したい項目もselectに含めるようにします。
条件はご自由に。全件Upsertにならないように更新用フラグとか何かしらの条件をつけたいところ・・・

const parents = await fromConn
    .sobject('Parent__c')
    .select('ExternalId__c, Name')
    .where({ ExternalId__c: { $ne: null } })

送信先に親レコードをUpsert

loadAndWaitForResultsメソッドを利用して送信先にUpsertをかけます。こちらのメソッドはbeta版であるバージョン2.0以降から利用できるようです。
使用している引数の内容は以下の通りです。

  • object: 更新対象のオブジェクトAPI名
  • operation: 更新方法です。以下の中から選択します。
    • insert, update, upsert, delete, hardDelete, quer, queryAll
  • input: 更新に使用するデータ
  • externalIdFieldName: 外部Idの項目API名です。Upsertの際は指定します。
  • pollTimeout: デフォルトだとタイムアウトするので長くしておくことをおすすめします。
const parentResults = await toConn.bulk2.loadAndWaitForResults({
    object: 'Parent__c',
    operation: 'upsert',
    input: parents,
    externalIdFieldName: 'ExternalId__c',
    pollTimeout: 1800000,
  })
  console.dir(parentResults, { depth: null })

送信元から子レコードを取得

親レコードの時と大体同じです。
やや異なる部分はParent__r.ExternalId__cくらいです。
Updateのみであればあまり深く考えなくていいのですが、Sandboxで新規作成されたレコードがある場合、本番にInsertした結果の本番レコードIdが必要になりますので、それを取得するために使います。

const childs = await fromConn
    .sobject('Child__c')
    .select('ExternalId__c, Name, Parent__c, Parent__r.ExternalId__c')
    .where({ ExternalId__c: { $ne: null } })

親レコードのUpsert結果を元に送信先に子レコードをUpsert

  let newChilds: typeof childs = []
  parentResults.successfulResults.forEach(result => {
    const parentResult: UpsertResult & Partial<Fields$Parent__c> = result

    const filterChilds = childs.filter(child => {
      return child.Parent__r?.ExternalId__c === parentResult.ExternalId__c
    })
    newChilds = [
      ...newChilds,
      ...filterChilds.map(child => {
        delete child.Parent__r
        return { ...child, Parent__c: parentResult.sf__Id }
      }),
    ]

  const childResult = await toConn.bulk2.loadAndWaitForResults({
    object: 'Child__c',
    operation: 'upsert',
    input: newChilds,
    externalIdFieldName: 'ExternalId__c',
    pollTimeout: 1800000,
  })
  console.dir(childResult, { depth: null })

親レコードのUpsert結果parentResults.successfulResultsでループをします。

resultの型は以下だし、結果には返ってきてるので親オブジェクトの項目にそのままアクセスできるかと思ったのですが、上手いことできなかったので形を整えます。いい感じにできる方法があればそれで構いません。その時は私にも教えてください!

result: {
    sf__Created: "true" | "false";
    sf__Id: string;
} & MySchema

まずはresult型を先程の形で定義します。

type UpsertResult = {
    sf__Created: 'true' | 'false'
    sf__Id: string
  }

そして定義した型とjsforceのschemaから引っ張ってきたFieldsにresultを当て込みます。

const parentResult: UpsertResult & Partial<Fields$Parent__c> = result

次に子レコードの更新に使うデータを作成します。
このままでは最後のループで全部上書きされてしまうので親レコードと繋がりのある子レコードだけをフィルタリングしたものをつくります。

データを作る際、親レコードへの参照を持つ項目の値(今回でいうParent__c)をresultのsf__Idに置き換えます。これでInsert分にも対応することができます。
注意ですがdelete child.Parent__rをしておかないとUpsertされないはずです。数式やリレーションなどの更新できないような項目を最初のクエリに含めていた場合はこの段階で削除しておきます。
この辺ももうちょっとかっこよくやりたいですね・・・

    const filterChilds = childs.filter(child => {
      return child.Parent__r?.ExternalId__c === parentResult.ExternalId__c
    })
    
    newChilds = [
      ...newChilds,
      ...filterChilds.map(child => {
        delete child.Parent__r
        return { ...child, Parent__c: parentResult.sf__Id }
      }),
    ]

この後は子レコードをUpsertしています。親レコードの時と同じなので説明は省きます。
3段階以上の親子関係がある場合はChildのUpsert結果を元に同じようなことをやるだけでOKなはずです。

コード全文

合わせるとこんな感じになります。似たような処理は関数化していきたいですね。

export const releaseRecords = async (
  fromConn: SalesforceConnection,
  toConn: SalesforceConnection,
) => {
  type UpsertResult = {
    sf__Created: 'true' | 'false'
    sf__Id: string
  }

  const parents = await fromConn
    .sobject('Parent__c')
    .select('ExternalId__c, Name')
    .where({ ExternalId__c: { $ne: null } })

  const parentResults = await toConn.bulk2.loadAndWaitForResults({
    object: 'Parent__c',
    operation: 'upsert',
    input: parents,
    externalIdFieldName: 'ExternalId__c',
    pollTimeout: 1800000,
  })
  console.dir(parentResults, { depth: null })

  const childs = await fromConn
    .sobject('Child__c')
    .select('ExternalId__c, Name, Parent__c, Parent__r.ExternalId__c')
    .where({ ExternalId__c: { $ne: null } })

  let newChilds: typeof childs = []
  parentResults.successfulResults.forEach(result => {
    const parentResult: UpsertResult & Partial<Fields$Parent__c> = result

    const filterChilds = childs.filter(child => {
      return child.Parent__r?.ExternalId__c === parentResult.ExternalId__c
    })
    newChilds = [
      ...newChilds,
      ...filterChilds.map(child => {
        delete child.Parent__r
        return { ...child, Parent__c: parentResult.sf__Id }
      }),
    ]

  const childResult = await toConn.bulk2.loadAndWaitForResults({
    object: 'Child__c',
    operation: 'upsert',
    input: newChilds,
    externalIdFieldName: 'ExternalId__c',
    pollTimeout: 1800000,
  })
  console.dir(childResult, { depth: null })
}

おわり

jsforceを利用してUpsertを書いただけですが、結構簡単に実装できました。
以下のような課題があるのでいずれ機能追加していきたいです。

  • 更新するオブジェクトを選べない
    • コマンドライン引数でなんとか指定すれば良さそうです
  • 全件Upsertしかない
    • 更新用フラグをつけるか何かしらの日付条件をつけたりなどが考えられます
  • Deleteできない
    • 送信先にあって送信元にないレコードを取得して削除するか、論理削除で運用するか
  • ファイル未対応
    • ファイルのオブジェクト形式がめんどくさいですよね・・・

参考

jsforceでのupsertについて調べていたところこちらの記事を見つけました。
loadAndWaitForResultsの発見に至った記事です。ありがとうございます。
https://zenn.dev/yktakaha4/articles/salesforce_bulk2_with_jsforce

Terass Tech Blog

Discussion