Como buscar hierarquias de dados complexas em uma única requisição, paginar resultados via cursores e aposentar classes Apex de leitura rápida utilizando o poder declarativo do GraphQL.
Por anos no desenvolvimento em Lightning Web Components (LWC), quem precisava renderizar dados de múltiplos objetos relacionados em uma única visão esbarrava sempre no mesmo dilema de arquitetura:
- Imagine montar um card com dados da Conta, os últimos contatos principais e a listagem de oportunidades abertas com ticket alto.
Na prática do dia a dia, a arquitetura tradicional costumava empurrar duas abordagens cheias de atritos:
- O Controlador Apex Descartável: Criava-se uma classe Apex com
@AuraEnabled(cacheable=true)contendo SOQL com subconsultas e wrappers DTO. O custo disso no ciclo de vida é real: mais classes de teste para manter a régua dos 75%, deploys adicionais de backend a cada novo campo que o layout pedia e a preocupação constante com checagens de FLS eWITH USER_MODE. - A Cascata de Wires (
getRecordWaterfall): Tentava-se usar adaptadores isolados do Lightning Data Service (LDS) — umgetRecordpara Conta, aguardando o retorno para disparargetRelatedListRecordsde Contatos e Oportunidades. O resultado era uma sequência lenta de chamadas HTTP encadeadas na rede e excesso de dados trafegados.
Para resolver essa dor de forma elegante e alinhada aos padrões web modernos, a Salesforce consolidou o GraphQL Wire Adapter, pelo módulo lightning/graphql (versão 2).
Operando diretamente sobre a User Interface API (UI API) e gerenciado pelo cache inteligente do Lightning Data Service (LDS), o adaptador permite declarar a árvore exata de campos que a interface consome. Uma única requisição recupera entidades mestres e filhas, respeitando as permissões do usuário em nível de campo e objeto, com suporte nativo a paginação por cursores e sincronização de cache.
1. Topologia do lightning/graphql e Cache Normalizado

Ao contrário de chamadas REST ou métodos imperativos que devolvem dados como blocos estáticos de JSON, o GraphQL Wire Adapter trafega através do Lightning Data Service (LDS).
Isso traz um benefício imediato: a Normalização de Cache no Cliente.
Fluxo de Dados com Cache Normalizado:
1. Componente A executa consulta GraphQL:
└──> Recupera Conta "Acme" e seu Contato "Carlos (Email: carlos@acme.com)"
└──> O LDS armazena cada registro por ID no cache local do navegador
2. Componente B (em outra seção da página) atualiza o e-mail de "Carlos" via formulário nativo
3. Reatividade Automática:
└──> O LDS atualiza o nó no cache central
└──> O Componente A é notificado e re-renderiza o novo e-mail imediatamente
└──> Zero requisições extras de rede!
O Que Diferencia a v1 (lightning/uiGraphQLApi) da v2 (lightning/graphql)?
lightning/uiGraphQLApi(v1): Foi a primeira versão disponibilizada. Ainda é mantida por compatibilidade com execuções móveis desconectadas no Salesforce Mobile SDK.lightning/graphql(v2): É o padrão oficial atual. Traz suporte a variáveis reativas flexíveis, mutações declarativas e o utilitáriorefreshGraphQL.
2. Implementação Prática: Componente accountComprehensiveDashboard
Construímos um componente real para validar o adaptador na org de testes onlysalesforce_cgc_1. Ele busca os dados cadastrais da Conta, os Contatos relacionados e as Oportunidades em aberto acima de um valor mínimo, permitindo avançar de página por cursores.
Controlador JavaScript (accountComprehensiveDashboard.js)
Utilizamos a tag de modelo literal gql para compilar o grafo e conectamos o adaptador com a anotação @wire:
/* Data: 2026-10-13; Objetivo da Customização: Componente LWC demonstrando GraphQL Wire Adapter v2 com queries multi-objeto e paginação por cursores */
import { LightningElement, api, wire, track } from 'lwc';
import { gql, graphql, refreshGraphQL } from 'lightning/graphql';
const GET_ACCOUNT_GRAPH_QUERY = gql`
query getAccountCompleteProfile($accountId: ID!, $minAmount: Currency, $pageSize: Int, $afterCursor: String) {
uiapi {
query {
Account(where: { Id: { eq: $accountId } }) {
edges {
node {
Id
Name {
value
}
AnnualRevenue {
displayValue
}
Rating {
value
}
Contacts(first: 5, orderBy: { LastName: { order: ASC } }) {
edges {
node {
Id
Name {
value
}
Title {
value
}
Email {
value
}
}
}
}
Opportunities(
first: $pageSize
after: $afterCursor
where: {
and: [
{ IsClosed: { eq: false } },
{ Amount: { gte: $minAmount } }
]
}
orderBy: { Amount: { order: DESC } }
) {
totalCount
pageInfo {
hasNextPage
endCursor
}
edges {
node {
Id
Name {
value
}
StageName {
value
}
Amount {
displayValue
}
}
}
}
}
}
}
}
}
}
`;
export default class AccountComprehensiveDashboard extends LightningElement {
@api recordId;
@track minimumAmountThreshold = 10000;
@track pageSize = 3;
@track currentCursor = null;
get graphqlVariables() {
return {
accountId: this.recordId,
minAmount: this.minimumAmountThreshold,
pageSize: this.pageSize,
afterCursor: this.currentCursor
};
}
@wire(graphql, {
query: GET_ACCOUNT_GRAPH_QUERY,
variables: '$graphqlVariables'
})
graphqlQueryResult;
get accountData() {
const edges = this.graphqlQueryResult?.data?.uiapi?.query?.Account?.edges;
return (edges && edges.length > 0) ? edges[0].node : null;
}
get contactsList() {
const contactEdges = this.accountData?.Contacts?.edges || [];
return contactEdges.map(edge => edge.node);
}
get opportunitiesList() {
const oppEdges = this.accountData?.Opportunities?.edges || [];
return oppEdges.map(edge => edge.node);
}
get pageInfo() {
return this.accountData?.Opportunities?.pageInfo;
}
get hasNextPage() {
return this.pageInfo?.hasNextPage || false;
}
get isNextPageDisabled() {
return !this.hasNextPage;
}
get isLoading() {
return !this.graphqlQueryResult?.data && !this.graphqlQueryResult?.errors;
}
get errorMessage() {
if (this.graphqlQueryResult?.errors) {
return this.graphqlQueryResult.errors.map(err => err.message).join(', ');
}
return null;
}
handleNextPage() {
if (this.hasNextPage) {
this.currentCursor = this.pageInfo.endCursor;
}
}
handleResetPagination() {
this.currentCursor = null;
}
handleRefresh() {
refreshGraphQL(this.graphqlQueryResult);
}
}Atenção à regra de sintaxe de templates no Winter ’27: Expressões unárias como
disabled={!hasNextPage}em atributos de template disparam erro de compilaçãoLWC1208. A boa prática é sempre encapsular a lógica booleana em um getter (comoget isNextPageDisabled()).
Template HTML (accountComprehensiveDashboard.html)
<!-- Data: 2026-10-13; Objetivo da Customização: Template LWC para exibição declarativa de grafo de dados via GraphQL -->
<template>
<lightning-card title="Painel de Conta 360 (Via GraphQL Nativo)" icon-name="standard:account">
<div class="slds-p-around_medium">
<template lwc:if={isLoading}>
<div class="slds-align_absolute-center slds-p-around_large">
<lightning-spinner alternative-text="Carregando grafo de dados..." size="medium"></lightning-spinner>
</div>
</template>
<template lwc:if={errorMessage}>
<div class="slds-notify slds-notify_alert slds-theme_error slds-m-bottom_medium" role="alert">
<span>{errorMessage}</span>
</div>
</template>
<template lwc:if={accountData}>
<div class="slds-box slds-theme_shade slds-m-bottom_medium">
<h2 class="slds-text-heading_medium">{accountData.Name.value}</h2>
<p class="slds-text-body_regular">Receita Anual: {accountData.AnnualRevenue.displayValue}</p>
<p class="slds-text-body_regular">Classificação: {accountData.Rating.value}</p>
</div>
<h3 class="slds-text-heading_small slds-m-bottom_small">Contatos Principais ({contactsList.length})</h3>
<div class="slds-grid slds-wrap slds-gutters slds-m-bottom_large">
<template for:each={contactsList} for:item="contact">
<div key={contact.Id} class="slds-col slds-size_1-of-1 slds-size_1-of-2-medium slds-m-bottom_small">
<div class="slds-box slds-box_x-small">
<p class="slds-text-title_bold">{contact.Name.value}</p>
<p class="slds-text-body_small">{contact.Title.value}</p>
<p class="slds-text-body_small slds-text-color_weak">{contact.Email.value}</p>
</div>
</div>
</template>
</div>
<div class="slds-border_top slds-p-top_medium">
<div class="slds-grid slds-grid_align-spread slds-m-bottom_small">
<h3 class="slds-text-heading_small">Oportunidades em Aberto (> R$ 10k)</h3>
<div>
<lightning-button label="Primeira Página"
variant="neutral"
size="small"
onclick={handleResetPagination}
class="slds-m-right_x-small">
</lightning-button>
<lightning-button label="Próxima Página"
variant="brand"
size="small"
disabled={isNextPageDisabled}
onclick={handleNextPage}>
</lightning-button>
</div>
</div>
<template for:each={opportunitiesList} for:item="opp">
<div key={opp.Id} class="slds-box slds-box_x-small slds-m-bottom_x-small slds-grid slds-grid_align-spread">
<div>
<span class="slds-text-title_bold">{opp.Name.value}</span>
<span class="slds-badge slds-m-left_small">{opp.StageName.value}</span>
</div>
<span class="slds-text-title_bold slds-text-color_success">{opp.Amount.displayValue}</span>
</div>
</template>
</div>
</template>
</div>
</lightning-card>
</template>3. Evidência Real
O componente foi implantado com sucesso na org de testes conectada via Salesforce CLI:

4. Como Funciona a Paginação por Cursores (Cursor Connections)
Ao contrário de consultas SOQL tradicionais que utilizam OFFSET (limitadas a 2.000 linhas e sujeitas a degradação de tempo de resposta com o crescimento da tabela), o GraphQL no Salesforce implementa a especificação formal de conexões com cursores:
- A primeira página requisita os primeiros itens com
first: $pageSize. - O resultado traz o objeto
pageInfo:hasNextPage: trueendCursor: "YXJyYXljb25uZWN0aW9uOjQ="(um token Base64 opaco que atua como ponteiro do último item).
- Ao solicitar a página seguinte, você passa o cursor no argumento
after: $afterCursor. - O mecanismo salta direto para o próximo nó em tempo O(1), sem varreduras lineares na base de dados.
5. Matriz de Decisão: Quando Usar GraphQL vs Apex
| Cenário de Negócio | Abordagem Recomendada | Justificativa de Arquitetura |
|---|---|---|
| Leitura de Objetos Relacionados para a Interface | GraphQL Wire Adapter | Elimina classes Apex, classes de teste e respeita FLS nativamente. |
| Grades Visuais com Filtros e Ordenação Dinâmica | GraphQL Wire Adapter | Filtros em where: e ordenações em orderBy: são nativos e declarativos. |
| Paginação em Listas com Alto Volume de Registros | GraphQL Wire Adapter | Paginação por cursores elimina os gargalos de OFFSET do SOQL. |
| Transações Complexas com Múltiplos DMLs em Cadeia | Controlador Apex | Gravações com rollback transacional integrado pertencem ao backend. |
| Cálculos Matemáticos Pesados e Algoritmos Complexos | Controlador Apex | O frontend não deve sobrecarregar a CPU do cliente com regras densas. |
| Integração com APIs Externas (Callouts) | Controlador Apex | O GraphQL do LWC consulta exclusivamente a UI API interna da Salesforce. |
Conclusão
A consolidação do GraphQL Wire Adapter (lightning/graphql) no LWC marca uma mudança de paradigma bem-vinda para o ecossistema Salesforce.
Ao transferir a modelagem de leitura para consultas declarativas e confiar na inteligência de cache do Lightning Data Service, sua equipe elimina dezenas de controladores Apex que só existiam para transportar dados, reduz o débito técnico de testes unitários e entrega telas consideravelmente mais fluidas.
Já experimentou adotar o lightning/graphql no lugar de controladores descartáveis nos seus projetos? Conte para nós sua experiência nos comentários!
