背景
Salesforceにおいて、DatatableとのRowに対するアクションは使用頻度と重要度が高いのは、Spreadsheetライクに利用したい需要があるからだと考えています。ただデータの量が多くなると、データの処理能力と表示スピードを考慮した設計にする必要があります。
Datatableの形としては2種類あります。
-
Datatable
でrow actionができるテーブルを作成する -
Iteration
で、擬似的テーブルを作成する
大量データの表示方法としても2種類あります。
-
Pagination
でoffsetをPage数を利用してインクリメント取得する -
Infinite loading
で、延々終わるまで取得する
また、データをインクリメント的な取得方法も二つあります。
- SOQLの
Offset
をすでに取得したデータ量で設定しクエリする -
Id
やCreatedDate
をインクリメントごとで再設定しそれ以降を取得する
今回は、Infinite Loadingを試してみました。
Offsetの最大は2000です。それ以上をSOQLの回避策はQueryMoreで、カーディナリティが高く、一意の値が多い項目を使用することを勧めています。
Workaround for offset 2000 limit on SOQL query (Salesforce)
SOAP API - queryMore (Salesforce)
OFFSET (Salesforce)
デモ
構成
- Lead
- Lwc * 1
- Apex * 1
下準備
前回の投稿のMockarooでダミーデータを作成
Mockarooでダミーデータを作成してみた
内容
まずデータの取得です。私はmapで包んで投げるのが好きです。parametersはひとつずつで問題ないのでそこはお好みで。
SOQL
SOQL (getLeads)
返すデータはLeadのリスト、変数はstringSetかdateSetでまとめて管理しています。データ数はLIMIT 50です。DateTimeはJavaScriptではyyyy-mm-ddT00:00:00.000Z
なので渡すMapの型がDateTimeであればそのままで問題ないですが、String型で渡す場合は、yyyy-mm-dd 00:00:00
に成形し渡した先でDateTime.valueof(string)としてDateTime型に戻す手間があります。今回は、dateSetがDateTimeのMapなのでそのままです。
@AuraEnabled
public static List<Lead> getLeads(String searchTerm, Map<String, String> stringSet, Map<String, DateTime> dateSet) {
DateTime lastCreatedDate = dateSet.get('lastCreatedDate');
String lastId = stringSet.get('lastId');
String ownerAlias = stringSet.get('ownerAlias');
String status = stringSet.get('status');
String leadSource = stringSet.get('leadSource');
String query = 'SELECT Id, Company, LastName, FirstName, Email, Phone, Status, LeadSource, Website, Industry, OwnerId, Owner.Alias, CreatedDate, CreatedBy.Alias, (SELECT Id, Type, OwnerDivision__c, Owner.Alias FROM Cases__r), (SELECT Id, CreatedDate, Type, CreatedById FROM Tasks ORDER BY CreatedDate DESC LIMIT 10) FROM Lead';
List<String> conditions = new List<String>();
conditions.add('Status != \'Unqualified\'');
conditions.add('IsConverted = false');
if (lastCreatedDate != null && String.isNotBlank(lastId)) {
conditions.add('(CreatedDate < :lastCreatedDate OR (CreatedDate = :lastCreatedDate AND Id < :lastId))');
}
if (String.isNotBlank(ownerAlias)) {
conditions.add('Owner.Alias = :ownerAlias');
}
if (String.isNotBlank(leadSource)) {
conditions.add('LeadSource = :leadSource');
}
if (String.isNotBlank(searchTerm)) {
String keyword = '%' + searchTerm + '%';
conditions.add('(Company LIKE :keyword OR Email LIKE :keyword OR LastName LIKE :keyword OR FirstName LIKE :keyword)');
}
if (!conditions.isEmpty()) {
query += ' WHERE ' + String.join(conditions, ' AND ');
}
query += ' ORDER BY CreatedDate DESC, Id DESC LIMIT 50';
system.debug(query);
return Database.query(query);
}
lastId
とlastCreatedDate
が、スクロールによりSpinnerが表示されるたびにDatatableのonloadMore
アクションで呼び出されたmethod内で最後のRecordから取得します。それを、loadDataへ投げて次の50件を取得してくるparameterになります。
LWC
Datatable
Datatable (html)
<lightning-datatable
key-field="id"
data={rows}
columns={columns}
show-row-number-column="true"
enable-infinite-loading
load-more-offset="20"
onloadmore={loadmoreData}>
</lightning-datatable>
Datatableでは、onloadMore
でテーブルの下部に近づいた時に起動するアクションを設定します。load-more-offset
は下部からどのくらい上かpixel単位で数値を設定できます。早めにアクションして欲しい場合は数値を大きくしますが、デフォルトは20pxです。enable-infinite-loading
は必須です。
参照
lightning-datatable (salesforce)
loadData
loadData (JS)
loadData() {
return new Promise((resolve, reject) => {
getLeads({searchTerm: this.searchTerm, stringSet: this.stringSet, dateSet: this.dateSet })
.then(data => {
if (data.length) {
const mappedData = this.mapData(data);
this.rows = (this.rows || []).concat(mappedData); // concatenate mapped data
}
resolve();
})
.catch(error => {
console.error(error);
})
.finally(() => {
if (this.rows && this.rows.length > 1) {
this.tempLastCreatedDate = this.rows[this.rows.length - 1].CreatedDate;
this.tempLastId = this.rows[this.rows.length - 1].Id;
} else {
this.tempLastCreatedDate = new Date();
this.tempLastId = null;
}
});
});
}
ここで取得したデータの一番最後のlastIdとlastCreatedDateを取得しています。
this.tempLastCreatedDate = this.rows[this.rows.length - 1].CreatedDate;
this.tempLastId = this.rows[this.rows.length - 1].Id;
loadData
で取得します。@wireでもconnectedCallbackでも実装は可能ですが、@wireがデータ取得が速すぎてspinnerが出ないほどでした。今回はデータ量が50件ごとなのとspinnerが出た方が取得している感が出るので、connectedCallbackを利用しました。
また、データをdatatableに合うようにmapをしますが、mapについてはcellをデータによっては着色したり、iconをTypeAttributeでclass付けしたりダイナミックな表現にするときに噛ませます。詳細は本投稿の最後で。
loadmoreData
loadMoreData (JS)
async loadmoreData(event) {
if (this.rows.length < 50) {
return; // the datatable frame is too short, so it will keep rendering loadmore unless to stop by return here
}
const { target } = event;
target.isLoading = true;
this.dateSet.lastCreatedDate = this.tempLastCreatedDate;
this.stringSet.lastId = this.tempLastId;
try {
await this.loadData();
target.isLoading = false;
} catch (error) {
console.error(error);
}
}
loadmoreData
は、スクロールでDatatableの下部に来た時に呼ばれるメソッドで、現在のテーブル上にあるデータが50を下回りテーブルの下部が表示されてしまう高さの場合、DatatableのloadしたらすぐDatatableの下部に触ると見なされ永遠とloadData()をcallしてしまいます。そこで再取得の制御のために、このreturnが必要です。
まとめ
TaskやEvent, Chatterなど明らかにCreatedDateが降順で表示されるべきもの、数値として2000で区切るものでもないものにはInfinite Loadingは選択肢としてありかなと感じました。
参照
lightning-datatable (salesforce)
Workaround for offset 2000 limit on SOQL query (Salesforce)
SOAP API - queryMore (Salesforce)
OFFSET (Salesforce)
関連投稿
Mockarooでダミーデータを作成してみた
Searchable Combobox 検索型選択リストを作ってみた (lwc)
github