Salesforce LWC學習(三十五) 使用 REST API實現不寫Apex的批量建立/更新資料

zero.zhang發表於2021-07-03

本篇參考:

https://developer.salesforce.com/docs/atlas.en-us.224.0.api_rest.meta/api_rest/resources_composite_composite.htm

https://developer.salesforce.com/docs/atlas.en-us.224.0.api_rest.meta/api_rest/resources_composite_sobject_tree.htm

https://developer.salesforce.com/docs/atlas.en-us.224.0.api_rest.meta/api_rest/resources_composite_sobjects_collections_update.htm

https://developer.mozilla.org/zh-CN/docs/Web/API/Fetch_API/Using_Fetch

salesforce零基礎學習(一百零三)專案中的零碎知識點小總結(五)

https://jeremyliberman.com/2019/02/11/fetch-has-been-blocked-by-cors-policy.html

我們在學習LWC的時候,使用 wire adapter特別爽,比如 createRecord / updateRecord,按照指定的格式,在前端就可以直接將資料的建立更新等操作搞定了,lwc提供的wire adapter使用的是 User Interface API來實現。當然,人都是很貪婪的,當我們對這個功能使用起來特別爽的時候,也在疑惑為什麼沒有批量的建立和更新的 wire adapter,這樣我們針對一些簡單的資料結構,就不需要寫apex class,這樣也就不需要維護相關的test class,不需要考慮部署的時候漏資源等等。那麼,針對批量資料的場景,是否有什麼方式可以不需要apex,直接前臺搞定嗎?當然可以,我們可以通過呼叫標準的rest api介面去搞定。

 ContactController.cls

public with sharing class ContactController {

    @AuraEnabled(cacheable=true)
    public static List<Contact> getContacts() {
        return [
            SELECT AccountId, Id, FirstName, LastName, Title, Phone, Email
            FROM Contact limit 10
        ];
    }

    @AuraEnabled
    public static string updateContacts(Object data) {
        List<Contact> contactsForUpdate = (List<Contact>) JSON.deserialize(
            JSON.serialize(data),
            List<Contact>.class
        );
        try {
            update contactsForUpdate;
            return 'Success: contacts updated successfully';
        }
        catch (Exception e) {
            return 'The following exception has occurred: ' + e.getMessage();
        }
    }
}

datatableUpdateExample.html

<template>
    <lightning-card title="Datatable Example" icon-name="custom:custom63">
        <div class="slds-m-around_medium">
            <template if:true={contact.data}>
                <lightning-datatable
                    key-field="Id"
                    data={contact.data}
                    columns={columns}
                    onsave={handleSave}
                    draft-values={draftValues}>
                </lightning-datatable>
            </template>
            <template if:true={contact.error}>
                <!-- handle Apex error -->
            </template>
        </div>
    </lightning-card>
</template>

datatableUpdateExample.js

import { LightningElement, wire, api } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
import { refreshApex } from '@salesforce/apex';

import { ShowToastEvent } from 'lightning/platformShowToastEvent';

import updateContacts from '@salesforce/apex/ContactController.updateContacts';


const COLS = [
    { label: 'First Name', fieldName: 'FirstName', editable: true },
    { label: 'Last Name', fieldName: 'LastName', editable: true },
    { label: 'Title', fieldName: 'Title' },
    { label: 'Phone', fieldName: 'Phone', type: 'phone' },
    { label: 'Email', fieldName: 'Email', type: 'email' }
];
export default class DatatableUpdateExample extends LightningElement {
    columns = COLS;
    draftValues = [];

    @wire(getContacts)
    contact;

    async handleSave(event) {
        const updatedFields = event.detail.draftValues;

        await updateContacts({data: updatedFields})
        .then(result => {
            this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Success',
                    message: 'Contact updated',
                    variant: 'success'
                })
            );

            // Display fresh data in the datatable
            refreshApex(this.contact).then(() => {
                this.draftValues = [];
            });
       }).catch(error => {
           console.log(JSON.stringify(error));
           if(error.body) {
               console.log(JSON.stringify(error.body));
           } else if(error.detail) {
               console.log(JSON.stringify(error.detail));
           }
           this.dispatchEvent(
                new ShowToastEvent({
                    title: 'Error updating or refreshing records',
                    //message: error.body.message,
                    variant: 'error'
                })
            );
        });
    }
}

 結果展示:

點選以後

 我們在上一篇講述了標準的rest api,那OK,我們可以嘗試不適用後臺apex方式去搞定,而是在前臺通過rest api去玩一下,說到做到,開弄。後臺 apex增加獲取session的方法

public with sharing class ContactController {

    @AuraEnabled(cacheable=true)
    public static String getSessionId() {
        return UserInfo.getSessionId();
    }

    @AuraEnabled(cacheable=true)
    public static List<Contact> getContacts() {
        return [
            SELECT AccountId, Id, FirstName, LastName, Title, Phone, Email
            FROM Contact limit 10
        ];
    }
}

前端 html / javascript也同樣的改造一下

import { LightningElement, wire, api, track } from 'lwc';
import getContacts from '@salesforce/apex/ContactController.getContacts';
import { refreshApex } from '@salesforce/apex';

import { ShowToastEvent } from 'lightning/platformShowToastEvent';

import updateContacts from '@salesforce/apex/ContactController.updateContacts';
import getSessionId from '@salesforce/apex/ContactController.getSessionId';

const COLS = [
    { label: 'First Name', fieldName: 'FirstName', editable: true },
    { label: 'Last Name', fieldName: 'LastName', editable: true },
    { label: 'Title', fieldName: 'Title' },
    { label: 'Phone', fieldName: 'Phone', type: 'phone' },
    { label: 'Email', fieldName: 'Email', type: 'email' }
];
export default class DatatableUpdateExample extends LightningElement {
    columns = COLS;
    draftValues = [];
    @track isShowSpinner = false;
    @track sessionId;

    @wire(getContacts)
    contact;

    handleSave(event) {
        this.isShowSpinner = true;
        const updatedFields = event.detail.draftValues;
        updatedFields.forEach(item => {
            item.attributes = {"type" : "Contact"};
        });
        let requestBody = { "allOrNone": false, "records": updatedFields };
        console.log(JSON.stringify(updatedFields));
        getSessionId()
        .then(result => {
            this.sessionId = result;
            fetch('/services/data/v50.0/composite/sobjects/',
        {
            method: "PATCH",
            body: JSON.stringify(requestBody),
            headers: {
                    "Content-Type": "application/json",
                    "Authorization": "Bearer " + this.sessionId
                }
            }).then((response) => {
                //TODO 可以通過 status code判斷是否有超時或者其他異常,如果是200,則不管更新成功失敗,至少response回來
                console.log(response.status);
                return response.json(); // returning the response in the form of JSON
            })
            .then((jsonResponse) => {
                console.log('jsonResponse ===> '+JSON.stringify(jsonResponse));
                if(jsonResponse) {
                    jsonResponse.forEach(item => {
                        if(item.success) {
                            console.log(item.id + 'update success');
                        } else {
                            console.log(item.id + 'update failed');
                        }
                    })
                }
                refreshApex(this.contact).then(() => {
                    this.draftValues = [];
                });
                this.isShowSpinner = false;

            })
            .catch(error => {
                console.log('callout error ===> '+JSON.stringify(error));
                this.isShowSpinner = false;
            })
        })
        .catch(error => {
            //TODO
            console.log('callout error ===> '+JSON.stringify(error));
            this.isShowSpinner = false;
        })
        
    }
}

對應html

<template>
    <lightning-card title="Datatable Example" icon-name="custom:custom63">
        <div class="slds-m-around_medium">
            <template if:true={contact.data}>
                <lightning-datatable
                    key-field="Id"
                    data={contact.data}
                    columns={columns}
                    onsave={handleSave}
                    draft-values={draftValues}>
                </lightning-datatable>
            </template>
            <template if:true={contact.error}>
                <!-- handle Apex error -->
            </template>
        </div>
    </lightning-card>
    <template if:true={isShowSpinner}>
        <lightning-spinner alternative-text="Loading" size="medium"></lightning-spinner>
    </template>
</template>

執行展示:通過下圖可以看到報錯了CORS相關的錯誤,因為跨域進行了請求,這種情況的處理很單一也不麻煩,只需要 setup去配置相關的CORS以及CSP trust site肯定沒有錯

 下圖是配置的CSP 以及CORS

 

 但是很遺憾的是,即使配置了這些內容,還是不可以。也徵集了群裡大神的各種建議意見,各種嘗試擴充了 request header,發現還是不行。因為準備備考integration,所以也就暫時擱置了這個嘗試。週末時間相對充裕,不太甘心的忽然想到了一個事情,不要只看 console的報錯,檢視一下network是否有什麼有用的資訊。

通過這個截圖我們可以看出來,這個http 操作有三次的請求,第一次是跨域的檢查,request method是option,感興趣的可以自己檢視

 進行了錯誤的這次請求的展開,將 response內容展開,發現了問題

 好傢伙,儘管console報錯是CORS,但是其實這個問題的rootcause是 請求返回的code是401未授權,開啟 rest api 文件檢視一下

破案了,後臺通過 UserInfo.getSessionId獲取的session資訊無法用於REST API的授權,這裡就會有一個疑問,因為艾總髮過來了一個VF的demo,是可以通過rest去呼叫的,難道是vf / lex哪裡有區別,或者session有區別?

然後我就做了一個vf去列印一下session資訊以及通過apex在lex展示session資訊,發現visualforce page通過 GETSESSIONID或者 {!$Api.Session_ID}獲取的session id資訊和apexclass獲取的session id不一致,並且 vf 獲取的是可用的。OK,找到了解決方案以後,進行demo的bug fix。

GenerateSessionId.page

<apex:page contentType="application/json">
    {!$Api.Session_ID}
</apex:page>

ContactController: 只需要修改 getSessionId方法即可

@AuraEnabled(cacheable=true)
    public static String getSessionId() {
        return Page.GenerateSessionId.getContent().toString().trim();
    }

驗證:搞定

總結:篇中只展示了一下通過 REST API去批量運算元據的可行性,僅作為一個簡單的demo很多沒有優化,異常處理,錯誤處理等等。而且對資料量也有要求,200以內。如果感興趣的小夥伴歡迎自行去進行優化,希望以後有相關需求的小夥伴可以避免踩坑。篇中有錯誤的地方歡迎指出,有不懂歡迎留言。

相關文章