LWCでGraphQL使ってみた

LWCで使用できるGraphQLアダプタについて、実際にモノを作って解説してみた。

はじめに

Salesforce では、LWC からGraphQL API が使用でき、従来の REST API よりも柔軟にデータを扱えるようになりました。これまで複数のエンドポイントを呼び出していた処理も、GraphQL を使うことでシンプルにまとめられ、フロント側の実装負荷を大きく減らせるようになっています。

ここでは、GraphQL を LWC から利用する際の主なポイントを、できるだけ簡単にまとめてご紹介します。

GraphQL APIについては、下記記事をご覧ください。
https://base.terrasky.co.jp/articles/bwb6a

主な特徴

・LWCで使用することができる

標準のWireアダプタ(lightning/uiGraphQLApi)を使って、LWCから直接データベースにアクセスすることができます。そのため、簡単なデータ取得であればわざわざApexクラス(テストクラス)を作る必要がなく、JavaScriptだけで完結させることができます。
※Winter '26 リリースノートより※
Wireアダプタについて、「lightning/uiGraphQLApi」を使用するより「lightning/graphql」を使用することが推奨されています。
詳細:https://help.salesforce.com/s/articleView?id=release-notes.rn_api_graphql.htm&release=258&type=5

・セキュリティの自動適用

実行ユーザーのアクセス権限(プロファイルや権限セット)、項目レベルセキュリティ、共有ルールが自動的に適用されます。

・1回のリクエストで複数リソース・関連データをまとめて取得

1回のリクエストで主オブジェクトや、それに関連するオブジェクトのレコードを取得し、画面に表示することができます。

・レスポンスはクエリと同じ構造で返ってくる

クエリで書いたネストが、そのままJSONオブジェクトの形になり、欲しい形そのままでデータが返されるため、JavaScript での扱いがシンプルになります。

実際に使ってみた

検索欄に取引先名を入力して、取引先責任者が出てくるサンプルを作成したので、
それを参考に見ていきます。(GraphQLがメインですので、処理詳細については説明省きます)
import { LightningElement, wire, api } from "lwc";
import { gql, graphql } from "lightning/uiGraphQLApi";

const QUERY = gql`
    query getAccountContact( $accountName: String) {
        uiapi {
            query {
                Account(
                    where: {Name: { like: $accountName }}
                ) {
                    edges{
                        node{
                            Id
                            Name{ value }
                            Contacts{
                                edges{
                                    node{
                                        Name{ value }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
`;

export default class GraphQL extends LightningElement {
    rawResults;
    errors;
    @api recordId;
    cardTitle = "取引先責任者情報";
    
    // ユーザー入力の値を保持するプロパティを追加
    searchTerm; 

    // wireサービスで使用する動的な変数を定義
    get variables() {
        return {
            // 入力された検索語をGraphQL変数にバインド
            accountName: '%' + this.searchTerm + '%'
        };
    }

    // 入力フィールドの変更を処理するハンドラー
    handleSearchTermChange(event) {
        // 入力値が変更されるたびに searchTerm を更新し、wireサービスが自動的に再実行
        // `%` を付与することで部分一致検索(LIKE句)として機能
        this.searchTerm = event.target.value;
        
        // データをクリアして、新しい検索結果が表示されるのを待つ
        this.rawResults = undefined;
        this.errors = undefined;
    }

    @wire(graphql, {
        query: "$recordQuery",
        variables: "$variables"
    })
    graphqlQueryResult({ data, errors }) {
        if (data) {
            console.log('data:', data);
            this.rawResults = data;
        }
        if (errors) {
            console.error('error:', errors);
            this.errors = errors;
        }
    }

    get recordQuery() {
        return this.recordId ? QUERY : undefined;
    }

    get accounts() {
        if (this.rawResults?.uiapi?.query?.Account?.edges) {
            return this.rawResults.uiapi.query.Account.edges.map(edge => {
                const account = edge.node;
                const contactNodes = account.Contacts?.edges || [];

                return {
                    id: account.Id,
                    name: account.Name.value,
                    contacts: contactNodes.map(contactEdge => contactEdge.node.Name.value)
                };
            });
        }
        return [];
    }

    get hasData() {
        return this.accounts.length > 0;
    }

    get hasError() {
        return this.errors;
    }
}
graphQL.js
コネクタのインポート
import { gql, graphql } from "lightning/uiGraphQLApi";
graphQL.js
LWCでGraphQLを使用する際、必ず lightning/uiGraphQLApi モジュールからこの2つをインポートします。
クエリ
const QUERY = gql`
    query getAccountContact( $accountName: String) {
        uiapi {
            query {
                Account(
                    where: {Name: { like: $accountName }}
                ) {
                    edges{
                        node{
                            Id
                            Name{ value }
                            Contacts{
                                edges{
                                    node{
                                        Name{ value }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
`;
graphQL.js
gqlにテンプレートリテラルを渡してクエリを宣言します。クエリに渡す変数は「$varName」の形で宣言します。
GraphQLでは、データはリスト形式で直接返ってくるのではなく、必ず以下のルールで格納されています。
・edges: データのまとまり(リスト全体)
・node: その中のレコード(実データ)

Name { value } と書いているのは、表示用の値(Value)や表示ラベル(DisplayValue)を区別して取得できる仕様のためです。基本は { value } を指定します。

また、取引先の node の中に、子リレーションである 「Contacts」を記述することで、1回のクエリで親(取引先)とその子供(取引先責任者)を階層構造のまままとめて取得することができます。
Wireアダプタを使ってデータを取得
    // wireサービスで使用する動的な変数を定義
    get variables() {
        return {
            // 入力された検索語をGraphQL変数にバインド
            accountName: '%' + this.searchTerm + '%'
        };
    }

    // 入力フィールドの変更を処理するハンドラー
    handleSearchTermChange(event) {
        // 入力値が変更されるたびに searchTerm を更新し、wireサービスが自動的に再実行
        // `%` を付与することで部分一致検索(LIKE句)として機能
        this.searchTerm = event.target.value;
        
        // データをクリアして、新しい検索結果が表示されるのを待つ
        this.rawResults = undefined;
        this.errors = undefined;
    }

    @wire(graphql, {
        query: "$recordQuery",
        variables: "$variables"
    })
graphQL.js
"$variables" と文字列で指定することで、 動的に検索結果を描写することが可能です。
画面描写するために加工
get accounts() {
    if (this.rawResults?.uiapi?.query?.Account?.edges) {
        return this.rawResults.uiapi.query.Account.edges.map(edge => {
            const account = edge.node;
            const contactNodes = account.Contacts?.edges || [];

            return {
                id: account.Id,
                name: account.Name.value,
                contacts: contactNodes.map(contactEdge => contactEdge.node.Name.value)
            };
        });
    }
    return [];
}
graphQL.js
取得データはクエリと同じ構造のJSONオブジェクトとして「data」プロパティに格納されます。 SalesforceのGraphQLは「edges」や「node」を含む深い構造になるため、mapなどを使用して、画面表示に適したシンプルな構造へ変換処理を行います。
画面に取得した値を渡す
<template>
    <lightning-card title={cardTitle} icon-name="standard:contact">
        <div class="slds-p-around_medium">
            
            <lightning-input
                label="取引先名で検索"
                value={searchTerm}
                onchange={handleSearchTermChange}
                placeholder="例: America"
                class="slds-m-bottom_large"
            ></lightning-input>

            <template lwc:if={hasError}>
                <div class="slds-text-color_error">
                    <p>データの読み込み中にエラーが発生しました。</p>
                    <p>{errors}</p>
                </div>
            </template>

            <template lwc:else>
                <template lwc:if={hasData}>
                    <template for:each={accounts} for:item="account">
                        <div key={account.id} class="slds-card slds-p-around_small slds-m-bottom_medium">
                            <h3 class="slds-text-heading_medium slds-m-bottom_x-small">
                                取引先名: **{account.name}**
                            </h3>
                            
                            <template lwc:if={account.contacts.length}>
                                <p class="slds-text-heading_small slds-m-bottom_x-small">
                                    関連取引先責任者:
                                </p>
                                <ul class="slds-list_dotted slds-p-left_medium">
                                    <template for:each={account.contacts} for:item="contactName">
                                        <li key={contactName}>
                                            {contactName}
                                        </li>
                                    </template>
                                </ul>
                            </template>
                            <template lwc:else>
                                <p>関連する取引先責任者はいません。</p>
                            </template>
                        </div>
                    </template>
                </template>
                <template lwc:else>
                    <p>指定された条件に一致する取引先が見つかりませんでした。</p>
                </template>
            </template>
        </div>
    </lightning-card>
</template>
graphQL.html
JavaScriptで加工したデータをプロパティに渡し、テンプレートの for:each で繰り返しレンダリングすれば完了です。

おわり

GraphQLは LWC開発において、複雑な加工をする場合はApexを使ったほうが良いかもしれません。
ですが、簡単な画面開発などはApexクラスを作成せずに、複雑なリレーションを含むデータをセキュアに取得することができますので、コード記述量やメンテナンスコストを削減して提案の幅を広げてみてはいかがでしょうか。

参考