the industrial

ブログと言うより自分のためのメモ以外の何モノでもないです。でも読んでくださってありがとうございます。

SFDX x Node.js でテストデータをサクッと登録するスクリプトを作成した話とハマった話、そして約5年ぶりにSFDC開発をするエンジニアによるSFDXやLwcなどに対する所感

最近数年ぶりに Salesforce を触る機会がありました。というかあります。

そこら辺については下記をご参照のほど、何卒よろしくお願いいたします。

prtimes.jp

appexchangejp.salesforce.com

みなさんテストデータってどうやって作ってますか?

アルプの Salesforce 開発におけるセットとしましては、下記あたりを使ってます。

  • ApexCode
  • Aura Component
  • Lightning Web Component
  • 他、既存機能色々

環境としましては、

  • Scratch組織
  • SFDX

なんかを使っており、Scratch組織を作成したタイミングでSFDXを利用してプロビジョニング(プロビジョニングと言っていいの?)しているのですが、動作環境などに使うテストデータを毎回手でポチポチと登録するのがめんどくさかったんですよね。

例えば、Account と Contact をセットとしたとき、色々なバリエーションが必要で毎回手で入力していたのですが、これがもう腱鞘炎になるレベル。

こういう特に何も考えずに行う作業はやはり自動化したいということで、 force:data:tree:import を使いました。

これはおそらく誰でもやっていることだと思いますし、今さら使い方を書いてもしょうがない(上記のリンクからドキュメント読んだほうが良い)ので詳しい使い方は割愛します。

スクリプト化しよう

作成するテストデータによりバリエーションを持たせたいという欲求が生まれてくるのも自然でして、そこで Node.js を使ってサクッとスクリプトを作成したろうという話です。

一応仕事で作ったスクリプトなので全容は控えるのですが、ざっくり書いておくと

const fs = require('fs')
const dateFormat = require('dateformat')
const { execSync } = require('child_process')

const justNow = dateFormat(new Date(), 'hhMMss')
fs.writeFileSync('./scripts/tmp/Accounts.json', fs.readFileSync('./scripts/testDataSources/Accounts.json', 'utf8').replace(/hhMMss/gi, justNow))
fs.writeFileSync('./scripts/tmp/Contacts.json', fs.readFileSync('./scripts/testDataSources/Contacts.json', 'utf8').replace(/hhMMss/gi, justNow))

execSync('cp -p scripts/testDataSources/Account-Contact-plan.json scripts/tmp/Account-Contact-plan.json')
execSync(`sfdx force:data:tree:import -p scripts/tmp/Account-Contact-plan.json -u ${options.org}`)

みたいな感じです。

解説

下記はテストデータの元として ./scripts/testDataSources/Accounts.json を読み込み、 hhMMss を現在時間に置換した後に ./scripts/tmp/Accounts.json に書き込みを行ってます。

fs.writeFileSync('./scripts/tmp/Accounts.json', fs.readFileSync('./scripts/testDataSources/Accounts.json', 'utf8').replace(/hhMMss/gi, justNow))

Account.json の中身はざっくりですが

{
  "records": [
    {
      "attributes": {
        "type": "Account",
        "referenceId": "hhMMss01_A"
      },
      "Name": "hhMMss01_アカウント名"
    },
    {
      "attributes": {
        "type": "Account",
        "referenceId": "hhMMss02_A"
      },
      "Name": "hhMMss02_アカウント名"
    }
  ]
}

となっておりまして、 .replace(/hhMMss/gi, justNow) でID値などを現在時分秒に置換することでユニークな値を作成しています。

Contact.json は下記のような感じです。こちらも Account.json 同様、ID値を置換してユニークな値を作成しています。

{
    "records": [
        {
            "attributes": {
                "type": "Contact",
                "referenceId": "hhMMss01_C"
            },
            "LastName": "hhMMss01_名字",
            "FirstName": "太郎",
            "Email": "foo_hhMMss01@omiend.com",
            "AccountId": "@hhMMss01_A"
        },
        {
            "attributes": {
                "type": "Contact",
                "referenceId": "hhMMss02_C"
            },
            "LastName": "hhMMss02_名字",
            "FirstName": "次郎",
            "Email": "baa_hhMMss02@omiend.com",
            "AccountId": "@hhMMss02_A"
        },
    ]
}

次に、

execSync('cp -p scripts/testDataSources/Account-Contact-plan.json scripts/tmp/Account-Contact-plan.json')

を実行してplanファイルをコピーし、

execSync(`sfdx force:data:tree:import -p scripts/tmp/Account-Contact-plan.json -u ${options.org}`)

にてimportを実行しています。

本当はもっと色々してますが、まあこんな感じですかねえ。

ハマったところ

Accountなどの referenceId に指定している hhMMss01_A ですが、初めは yyyymmddhhMMss01_A のようにして年月日時分秒を指定してました。

しかし、2回目以降どうしても下記エラーが出てしまいます。

{
    "hasErrors": true,
    "results": [
        {
            "referenceId": "2020032707565901_A",
            "errors": [
                {
                    "statusCode": "DUPLICATES_DETECTED",
                    "message": "Duplicate Alert",
                    "fields": []
                }
            ]
        }
    ]
}

年月日時分秒なので秒単位でユニークになるハズなのと、エラーメッセージが DUPLICATES_DETECTED のみなので何も言っていない(本当に何も言っていない)に等しく、まったく原因が分から無いままおそらく丸1日ほどハマっていました。

が、ふと原因に気づきました。

おそらくですが、ID値が長すぎてどこかで末尾が欠落し、例えば 2020032707565901_A20200327 の様になってしまったのではないかなと思います。

そのため、 yyyymmddhhMMss01_A ではなく hhMMss01_A にしている次第です。

また、 Name 属性もユニークにする必要があるようです。

とまあ、Node.js が使えるので基本的になんでも出来るのは良いなあと思いました。

DUPLICATES_DETECTED は文献を漁っても原因が分からなかっただけに、気づいた時は心底安心するとともに、もし同様のエラーでハマっている方のお力に少しでもなればと思います。

その文字列を観た感じ重複しているというのはワカルのですが、そんなハズ無いという思い込みが捨てられたらというのと、もう少し丁寧なエラーメッセージがほしいなと言ったところですかね笑

SFDX所感

僕がSFDCの開発に携わっていたのがもうかれこれ5年前だったのですが、Eclipse にSFDCのプラグインを入れて開発していました。

しかし、コードの補完が効きづらかったり、ソースコードのデプロイ速度が遅かったためローカルで書いたソースをブラウザにコピペして開発していたりと、お世辞にも良い開発体験ではなかったと思います。

SFDXはその頃の教訓をかなり活かせている気がします。

Visual Studio Code を基本として開発しているのですが、プラグインは良く出来ていますしデプロイ速度も当時に比べて格段に向上しています。

Lightning Web Component

Lightning Web Component はすんなり入りやすかったですね。

HTML, Script, CSS が別れている点が Vue に少し似ていると思いました。

Script は ECMAScript フレンドリーに記述できますし、HTMLとScriptの連携もとても楽です。

CSS はあまり書かないのですが、理由としては Lightning Design System が提供するコンポーネントを使えば(今の所)事足ります。

ただ、 Lwc について一つ思うのが @wire を利用する際に fields にフィールド名を指定するのは嫌な気がします。

↓こういうやつ。

    @wire(getRecord, { recordId: '$recordId', fields: [ACCOUNT_NAME_FIELD] })
    record;

実装上楽になるかもしれませんが、これだとフロントがSOQLを書いているという意味にもなりますので、そこはフロントの責務から剥がしたい気持ちがありました。

そのため、下記のように画面で必要なデータは connectedCallback で ApexCode を呼びだして、SOQL は ApexCode 側に寄せるように実装してます。

↓下記のイメージ

import getAccount from "@salesforce/apex/AccountController.getAccount"

@api recordId
@track account

async connectedCallback() {
  this.account = 
    await getAccount({ recordId: this.recordId })
      .catch(error => {
        console.error(error)
      })
}

しかし、そうするとやはり ApexCode はカオス化しやすい気がしますね。その点は設計思想などでカバーできればよいかな。

ただ、 @wire の存在を考えると、今後は Lwc の js 層が ApexCode に取って代わる未来にしたいのかなという考えもわかります(実際Salesforce社がどう考えているかはわからないです)。

おしまいに

久しぶりにSFDCの開発へと戻ってきたのですが、かなり浦島太郎状態で思ったよりも楽しいです。

というわけで、アルプ株式会社では、絶賛 Salesforce エンジニアを探しております。

気になったそこのあなた、お気軽にお問い合わせください。Salesforce エンジニアに限らず採用は行っておりますので、併せてご確認ください!

bosyu.me

thealp.co.jp