LoginSignup
2
3

Salesforce開発関連メモ

Last updated at Posted at 2022-04-12

初めに

Salesforce開発に関して、実際に実装が必要になった際に調べて役に立ったTipsを延々と紹介します(随時更新)

Apex関連

文字列配列の初期化&文字追加

  • Java
String[] str = {a, b, c};
List<String> strList = new List<String>(Arrays.asList("Apple", "Orange", "Melon"));
  • Apex
List<String> strList = new List<String>{'a', 'b', 'c'};

null、空文字チェック

オブジェクト項目の数値型をIntegerに変換

  • そのまま取得した数値型を obj__c.ItemNumber__c == 1とか比較したりはできない
System.assertEquals(Integer.valueOf(Account.Number_of_Contacts__c) ,1);

オブジェクトから取得したデータを元にループ

for (Account account : [select id from account]) {
    contacts.add(new Contact(firstname='first', lastname='last', accountId=account.id));
}

List型変数内の同じ値を数える

  • Mapを使う
  • String型にしているが、型を変えれば汎用的に使用OK(標準関数ない?)
public static Map<String, Integer> findDuplicateCount(List<String> targetStrList) {
    Map<String, Integer> cntMap = new Map<String, Integer>();

    for(String tmpKey : targetStrList) {
        if(!cntMap.containsKey(tmpKey)) {
            cntMap.put(tmpKey, 0);
        } else {
            cntMap.put(tmpKey, cntMap.get(tmpKey)+1);
        }
    }
    return cntMap;
}

Map型の指定したKey値からValue値を取得する

  • KeySet()を使用するとSet型のリストが返ってくる
  • Setをループすればいいだけだが、1Linerでやりたいとき(混乱するので丁寧な解説を)
    • KeySet()で取得するSetをList型に変換
    • Key値のList型変数になる
    • Index値を指定してKey値を取り出す
    • 指定のMapに対して get()でKeyを指定して値を取り出す
  • 例はindex0の値を取り出す例
System.debug('Current Key: ' + new List<String>(tmpMap.keySet()).get(0));
System.debug('Current Value By Key: ' + tmpMap.get(new List<String>(tmpMap.keySet()).get(0)));

Cloneメソッド

  • いきなり出てきて焦った。しかもDeepCloneもあるらしい。
    • Cloneは単純に現在のデータをCloneして、新しいIdで作り直す。
    • DeepCloneは関連リストも含めてCloneするらしい。
    • 参照: Clone and DeepClone
clone(preserveId, isDeepClone, preserveReadonlyTimestamps, preserveAutonumber))

レコードの一括作成

sObject型でデータを作ってListにaddしてinsertするだけ.

List<Contact> lstContact = new list<Contact>();

for(Account acc: lstAccounts)
{
    Contact objContact = new Contact(LastName = 'test', AccountId = acc.Id);
    lstContact.add(objContact );
}

if(!lstContact.isEmpty())
{
   insert lstContact ;
}

作成したレコードのIdの取得

  • フローとかでできるやつ、Apexでどうやる?ってなったが、Insertした後のレコード変数のIdにアクセスするだけだった。
Account acc = new Account(Name='Test');
insert acc;

system.debug("Inserted Account Id = " + acc.Id);

レコードの更新

  • リストの中のフィールドを更新するとき
List<Account> acc =[SELECT Id, Name FROM Account];
List<Account> newAcc = new List<Account>();
for(Account a : acc){
    a.Name ='Test';
    newAcc.add(a);
}

update newAcc; //upsertでも可

複数レコードの更新をする際に、成功したものはコミットさせたい場合

Database.SaveResult[]を使用してallOrNoneパラメータをfalseにしておく
普通にinsertを使用してしまうと一つでもエラーになった場合全てロールバックされるため

// Add Account
Account acct = new Account(
    Name='Test Account',
    NumberOfEmployees=10,
    BillingCity='Tokyo');
List<Database.SaveResult> results = Database.insert(acct, false);

ちなみに、upsertの場合は下記となります

List<Database.UpsertResult> results = Database.upsert(ops, false);

メソッドと返り値はこちらが詳しい:
https://qiita.com/t_yano/items/19f8462f41b2a2bafded

レコードの復元

  • RECORD_IDを復元したい再生レコードIdに変更すること。
  • 再生レコードIdは変更データキャプチャしてれば取得できるが、それ以外の方法はまだ調べていない
Employee__c record = [SELECT Id,Name FROM Employee__c WHERE Id='RECORD_ID' ALL ROWS];
undelete record;

乱数を作りたい場合

  • 下記のメソッドで作成。
  • 引数として UPPER_LIMITを渡しても良いし、Constにしても良いかと。
  • 帰ってきた値で配列のindexを指定すれば文字列やレコードなんかもいけます。
static final Integer UPPER_LIMIT = 3;
Integer getRandomNumber() {
    Integer rand = Math.round(Math.random()*1000);
    return Math.mod(rand, UPPER_LIMIT);
}

こちらのGitHubのソースを参考に:
GitHub: Get Random Number
あるいはこちら:
https://qiita.com/Supply-net/items/c3e63edd2a3a260f06ba

正規表現で日本語を見つける

.find()はBooleanが返却される。

String regex = '[\\p{IsHan}\\p{IsHiragana}\\p{IsKatakana}]';
Pattern regexPattern = Pattern.compile(regex);
Matcher regexMatcher = regexPattern.matcher(tmpStr);
if(regexMatcher.find()) {
    // ...
}

ユーザのタイムゾーンに合わせた時間を取得

TimeZone tz = UserInfo.getTimeZone();
DateTime dt = Datetime.now();
// offset分の秒をGMTタイムゾーンに足す
DateTime nowJST = dt.addSeconds(tz.getOffset(dt)/1000);

Datetime値からDate値を取得して計算

  • 例は1年足している
obj2.Date__c = obj1.CreatedDate.date().addYears(1);

Datetimeの差分を計算

  • 秒(/1000だけ)
  • 分(/1000/60)
  • 時間(/1000/60/60)
  • 日(/1000/60/60/24)などは計算で取得
Decimal days = decimal.valueOf(nowJST.getTime() - tmpSA.SchedStartTime.getTime())/1000/60/60/24;

ある時点のデータベース値を取得して、エラーが発生した場合はRollBackしたい場合

  • 下記の通りに実装。
Savepoint sp = Database.setSavePoint();

try {
    // logic
} catch(Exception ex) {
    Database.rollback(sp);
    return ex.getMessage();
}

あるオブジェクトの全ての項目API名を取得して、動的にSOQLを発行したい場合

  • SELECT * ...ができないので項目名をいちいち記載したくないとき
  • FieldDefinitionオブジェクトからAPI名を取得
  • SObject型なのでStringに変換
  • SOQLクエリをStringで作成しておき、Database.query()を実行
  • SELECTのカラム名の','挿入はString.join()を使用する
    public static List<Boat__c> getData(String dataId) {
        List<FieldDefinition> fieldApiNames = [SELECT QualifiedApiName FROM FieldDefinition WHERE EntityDefinition.QualifiedApiName = 'Data__c' ];
        List<String> fieldApiStrNames = new List<String>();
        for(FieldDefinition fieldApiName : fieldApiNames) {
            fieldApiStrNames.add(String.valueOf(fieldApiName.QualifiedApiName));
        }        
        String soql = 'SELECT ' + String.join(fieldApiStrNames, ',') + ' FROM Data__c';
        if(String.isNotBlank(dataId)) {
            soql += ' WHERE dataId__c = :dataId';
        }
        return Database.query(soql);
    }

GEOLOCATION型のレコードの取得

  • JSON型で返却したい、LatitudeとLongitudeがメソッドに渡される
  • SELECT時もGeolocation__latitude__sって感じにしないといけない
  • WHEREやORDER BYでDISTANCE()GEOLOCATION()を使用しないといけないらしい
    public static string getDataByLocation(Decimal latitude, Decimal longitude, Id dataId){
        String query = 
            'SELECT Id, Name, Geolocation__latitude__s, Geolocation__longitude__s' +
            'FROM Data__c' +
            'WHERE DataId__c = :dataId' + 
            'ORDER BY DISTANCE(Geolocation__c, GEOLOCATION(latitude, longitude), \'mi\')' +
            'LIMIT 10';
        return JSON.serialize(Database.query(query));
    }

組織の社内ユーザのみを取得したい場合

  • ちなみに、他 UserTypeはこちらを参考
  • GuestやCommunity Userなども取得可能
SELECT Id, Name, UserType FROM User WHERE UserType = 'Standard'

取得したSObject型リストをId型リストに変換する

List<Account> acc = [SELECT Id, Name FROM Account];
Set<Id> accIds = (new Map<Id, Account>(acc)).KeySet();

SOQLで取得した結果をMapに変換する

  • IdSObject型のMapに変換する
Map<Id, Product2> prds = new Map<Id, Product2>([SELECT Id, Name FROM Product2 WHERE Id IN :ids]);

組織のドメイン取得

  • 下記でString型で使用可能
  • 取得できるのは https://xxxx.salesforce.comまで
System.URL.getSalesforceBaseURL().toExternalForm();

// 例えばケースレコードのLinkなら
String caseLink = URL.getSalesforceBaseUrl().toExternalForm() + '/lightning/r/Case/' + targetCase.Id + '/view/';

Apex Test

HTTP Call Outのテストがしたい場合

  • Test Classに HttpCalloutMockを継承する
  • NGの場合も別途作成する
@isTest
global class HttpCallOutMockTestMethod implements HttpCalloutMock{
    global HttpResponse respond(HTTPRequest request) {
        HttpResponse response = new HttpResponse();
        response.setHeader('Content-Type', 'application/json');
        response.setStatus('OK');
        response.setStatusCode(201);
        return response;
    }
}
  • 下記の通り、TestクラスのMethodを使用してAssertをかける
  • 大概のTestロジックの書き方も下記に習うが、HttpCallOutの場合はTest.startTest()などが必要
@isTest
private class TestClassSample {
    @isTestSetup
    static void MakeData() {
        // テストデータの作成
        Opportunity opp = new Opportunity(Name='TestOK' ...);
        // Custom Settingなど、Calloutで認証情報などがある場合はここで取得
        // some logic here...
    }

    @isTest
    static void HttpCallOutSucessTest() {
        Opportunity opp = [SELECT Id, Name FROM Opportunity WHERE Name = 'TestOK'];
        List<Id> OppIds = new List<Id>();
        OppIds.add(opp.Id);

        // HttpCallOutでの結果を返すMockをセット
        Test.setMock(HttpCalloutMock.class, new ProjectCalloutServiceMock());
        Test.startTest();
           // 下記にテストしたいクラスのメソッドを記述
           testTargetClass.TestTargetMethod(oppIds);
        Test.stopTest();
    
        opp = [SELECT StageName FROM Opportunity WHERE Id =: opp.Id];
        // 期待する結果
        System.assertEquals('XXXXX', opp.StageName);
    }
}

SOQL

親から子オブジェクトの項目を取得するSOQL

  • 絶対忘れる。 子オブジェクトのquery分は()で囲んでおく。
List<Account> lstAccount = [SELECT Id, Number_of_Contacts__c, (SELECT Id FROM contacts) FROM account WHERE Id in :setId ];

子から親オブジェクトの項目を取得するSOQL

  • これもいつも忘れる。
SELECT FirstName, LastName, Account.Name FROM Contact

キューIDの取得

  • これも忘れるんだよなぁ, Groupオブジェクトに対して Typeを`Queueに指定。
Group queue = [SELECT Id FROM Group WHERE Name='Regional Dispatch' AND Type='Queue'];

キューに含まれるユーザIDの取得

  • 上記で取得したキューIDを使用
  • Map<Id, GroupMember>を使用して取得できる Idは実際のユーザIDにならないので注意.
List<GroupMember> userIdsInQueue = [SELECT UserOrGroupId FROM GroupMember WHERE GroupId = :queueId];

集計関数

  • 集計関数を使った際は返却されるデータ型がAggregateResultsのリスト型になる(なにそれ)
  • apexで結果を取得する際の例は下記
List<AggregateResult> results  = [SELECT Industry, count(Id) total
    FROM Account GROUP BY Industry];
for (AggregateResult ar : results) {
    System.debug('Industry: ' + ar.get('Industry'));
    System.debug('Total Accounts: ' + ar.get('total'));
}

ユーザに対してレコードアクセスがあるか確認

SELECT RecordId, HasEditAccess FROM UserRecordAccess WHERE UserId = [single ID] AND RecordId = [single ID]

Salesforce組織のオブジェクト名、フィールド名のAPI名を取得するSOQLクエリ

あるオブジェクトのアクセス権をプロファイルを指定して取得する

SELECT Id, Field, SObjectType, PermissionsRead, PermissionsEdit 
FROM FieldPermissions 
WHERE parentId IN ( SELECT id 
                    FROM permissionset 
                    WHERE PermissionSet.Profile.Name = 'システム管理者') AND SObjectType = 'Case'

Flow

Apex Classを呼び出す際

  • @InvocableMethodをメソッド前に記載する
  • 引数は1つしか渡せません (Innerクラスを作らない限りは)
public with sharing class GetRecordsByMultipleIds {
    @InvocableMethod
    public static List<List<Product2>> GetRecordsByMultipleIds(List<List<ID>> inputCollectionIds) {
//...
}

入力をCollection変数、出力も複数レコードになる場合

  • 非常に面倒な仕様です。
    • List<List<XXX>> varを引数とします。
    • Flowから渡したリストは var.get(0)で取得します。
    • Returnも List<List<XXX>> retValという風にしないといけないです。
public with sharing class GetRecordsByMultipleIds {
    @InvocableMethod
    public static List<List<Product2>> GetRecordsByMultipleIds(List<List<ID>> inputCollectionIds) {
        List<ID> queryIds = inputCollectionIds.get(0);
        List<Product2> products = [SELECT Id, Name, Family FROM Product2 WHERE Id IN:queryIds ORDER BY Name ASC];
        List<List<Product2>> result = new List<List<Product2>>();
        result.add(products);

        return result;
    }
}

Genericな引数と戻り値にしたい場合

FlowからApexクラスを呼び出す際、結構文字列操作したい場合ってないですか?
いちいちObjectの型を指定せずに、様々なオブジェクトで使いたい場合はよくやりそう。
例えばNameのフィールド値とか。

  • ちなみにこの場合、Flow側から入力/出力のオブジェクト型を指定することができます!
public with sharing class TestClass {
    @InvocableMethod
    public static List<List<SObject>> TestMethod(List<List<SObject>> records) {
        List<SObject> tmp = records.get(0);
        for(Integer i=0; i<tmp.size(); i++) {
            // SObjectのNameフィールドの値を取得する
            String name = String.valueOf(tmp.get(i).get('Name'));
            if(expression) {
                //...
            }
        }
        return result; //これはList<List<SObject>>のリスト
    }
{

フローからApexに複数の引数を渡したい場合

  • 引数、返却値を内部クラスに指定することで可能だった...!
  • Class内に複数変数を定義できるので、かなり自由度が上がる
  • 引数はClassのリスト型にする
public with sharing class IncrementQuantiyForDuplicatedSerialNo {
    @InvocableMethod
    public static List<FlowOutputs> updateQuantityForDuplicatedSerialNo(List<FlowInputs> inputs) {
        FlowInputs input = inputs.get(0);
        List<ProductItem> pi = new List<ProductItem>(input.recordList);
        String scannedSerialNo = input.serialNo;

        List<FlowOutputs> results = new List<FlowOutputs>();
        FlowOutputs rst = new FlowOutputs();

        for(ProductItem tmpPi : pi) {
            if(tmpPi.SerialNumber == scannedSerialNo) {
                tmpPi.QuantityOnHand += 1;
                rst.productName = tmpPi.ProductName;
                rst.serialNo = tmpPi.SerialNumber;
                rst.quantity = tmpPi.QuantityOnHand;
            }
        }

        rst.updatedList = new List<ProductItem>(pi);
        
        results.add(rst);
        System.debug('Return Value: Result ' + results);

        return results;
    }
    
    public class FlowInputs {
        @InvocableVariable
        public List<ProductItem> recordList;

        @InvocableVariable
        public String serialNo;
    }

    public class FlowOutputs {
        @InvocableVariable
        public List<ProductItem> updatedList;

        @InvocableVariable
        public String productName;

        @InvocableVariable
        public String serialNo;

        @InvocableVariable
        public Decimal quantity;
    }
}

画面フロー上にCSSを読み込みたい場合

  • 定数: テキストとして、CSSを入力する
変数名: sampleCss
background-color: #0076D2; border-radius: 30px; color: #fff; padding: 15px 60px; font-size: 18px;
  • テキストテンプレートを作成する
    • この際に、プレーンテキストを選択
変数名: button
<a href="" style="{!sampleCss}">応募する!</a>

下記みたいなのは作れる. (border-radius が効かない...)
image.png

Trigger

文法

  • Trigger.Newでループさせる。
  • Fireの条件が複数ある場合、Triggerのタイミングをキーに処理分岐も可能
// オブジェクト名指定、いつFireするかのタイミングを引数に指定
trigger TestTrigger on Opportunity (after insert, after update) {
    for(Opportunity opp : Trigger.New) {
        if(trigger.isInsert) {
            // ...
        }
        // ...
    }
}

トリガされた時点での変更前の値の取得

  • after updateの場合に使用するケースが多い
trigger TestTrigger on Case (after update) {
    for(Case record : Trigger.New) {
        Id priorId = Trigger.oldMap.get(record.Id).OwnerId;
        // logic
        if(priorId != record.OwnerId) {
            // ...
        }
    }
}

LWC

Lightning Buttonのwidthを100%にしたい際

slds-button_stretchをclassにapplyしても適用されない。
代わりにstyleでwidthを100%にする。

image.png

<lightning-button 
    label="検索"
    onclick={handleSearchKeyword}
    variant="brand"
    style="display: grid; width: 100%;">
</lightning-button>

LWCから画面フローを呼び出したい場合

  • Winter'23からついにLWC版の画面フロー呼び出しが可能に。

  • ハマってしまったのだが、html側からフローを呼び出す際、例えば入力変数がセットされていない場合はフラグなどでFlowを描画するかどうかのフラグを持ち、(<template if:true={renderFlow}>などで)Trueになって初めてフローを描画する形にした方が良い。

    • フローを描画するタイミングで確実に入力変数が渡されないと、変数のデータ型タイプエラーが発生することがある。
    • 例: onclickでボタンを押した際にデータ精査を行い、flowInputVariablesに値をセット、その後でフローを描画するなど
  • htmlに下記を記述(変数名は固定)

lwc.html
<template if:true={renderFlow}>
  <lightning-flow
      flow-api-name={flowApiName}
      flow-input-variables={flowInputVariables}
      onstatuschange={handleFlowStatusChange}>
  </lightning-flow>
</template>
lwc.js
//...
flowApiName = 'flow_name';
flowInputVariables = [];
renderFlow = false;
someFunc() {
    // ...
    this.flowInputVariables = [
        {
            name: 'recordId',
            type: 'String',
            value: this.selectedResourceId
        },
        {
            name: 'InvitationURLPrefix',
            type: 'String',
            value: this.InvitationURLPrefix
        },
    ];
    // 何かしらのロジックで描画を可にするフラグを用意
    this.renderFlow = true;
}

handleFlowStatusChange(event) {
	console.log("flow status : ", event.detail.status);
	if (event.detail.status === "FINISHED") {
		this.dispatchEvent(
			new ShowToastEvent({
				title: "成功",
				message: "サンプルメッセージ",
				variant: "success",
			})
		);
	}
}

画面フローからLWCを呼び出す際に、Collection変数を渡したい場合

  • js-meta.xmlファイルに対して、下記の通り記述する
  • type="String[]"を指定するとエラーがVSCode上では表示されるのだが、なぜかデプロイは成功する
sample.js-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>56.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">
            <property name="sampleNumbers" type="String[]" label="Sample Numbers"/>
            <property name="sampleTexts" type="String[]" label="Sample Texts"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
  • JavaScript側は xmlで指定した変数を @api化しておくだけ
sample.js
import { LightningElement, api } from 'lwc';

export default class DataTableOnFlow extends LightningElement {
    @api sampleNumbers;
    @api sampleTexts;
}
  • 画面フローに作成したLWCを配置する
  • LWCから、コレクション変数が選択できるようになっている

image.png

LWCから画面フローに対して値をアウトプットさせたい場合

  • 例えば、画面フロー上のLWCが読み込まれた場合に@wire apex あるいは 明示的Apexにてデータを取得してアウトプットとして画面フローに戻したい場合
  • 一例として、SObjectのリストを返す場合を想定
  • type"@salesforce/schema/SObjectName[]"を指定する
  • ちなみに、Genericな型にInputであればできる? 参考
    Outputは試したらエラーになりました。
sample.xml
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>56.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__FlowScreen</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__FlowScreen">
            <property name="returnData" type="@salesforce/schema/SObjectName[]" label="Return Data" role="outputOnly" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>
  • Apex側は、フローに返すのであれば、 List<List<SObject>>型にしないとイケさそうに思えるが、そもそも @InvocableMethod ではないので、xmlと同じで、返却の型は <List<SObject>とする。

  • JavaScript側は、Outputする変数は @apiにしておく

  • @wireのapexメソッドで返却される値に対して、@api変数を指定して、問題なく動作しました。

sample.js
import { LightningElement, api, wire } from 'lwc';
import testMethod from '@salesforce/apex/TestClass.testMethod';

export default class DataTableOnFlow extends LightningElement {

    @api returnData;

    @wire(testMethod, {strings: '$sampleTexts', nums: '$sampleNumbers'})
    wiredMethod({error, data}) {
        if(data) {
            this.returnData = data;
            this.error = undefined;
        } else if(error) {
            console.log('error')
            console.log(error)
        }
    }
}

参考記事

LWC上で音を出すアクションを作成

  • 例えばボタンが押されたら、音を出すなど
  • まずは、音声ファイルを静的リソースに登録する
  • Javacriptで下記の通り実装するだけ
// sound from static resource
import staticSuccessSound from '@salesforce/resourceUrl/SuccessSound';

export default class Sample extends LightningElement {
    successSound = new Audio(staticSuccessSound);

    connectedCallback() {
        this.successSound.play();
    }
}
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3