Resposta rápida
- O problema que resolve
- paginar em SQL depende do banco — o Protheus homologa SQL Server, Oracle e Postgres, cada um com sintaxe própria;
FWAdapterBaseV2faz o fonte não precisar escolher nenhuma delas. - Quem herda dela
- herdar
FWAdapterBaseV2é o padrão de quem escreve consulta paginada em TLPP, não a exceção. - Onde ela mora
- não é uma classe que se declara em código de aplicação para ler — o comportamento vem do que se observa nas chamadas.
- O nome real do método de mapa
AddMapFields, no plural —SetFieldMapeAddMapField, no singular, não existem.- A sequência obrigatória
Execute()→ se.T.→FillGetResponse()→GetJSONResponse().
Nesta página8 seções
Uma rota de listagem que devolve a tabela inteira de uma vez é um jeito
lento de travar quem consome. A saída é paginar dentro do banco — e aí o
Protheus esbarra no fato de o mesmo fonte rodar sobre SQL Server, Oracle ou
Postgres, cada um com sintaxe própria. FWAdapterBaseV2 é a resposta do
framework: monta a paginação, o filtro e o envelope, e deixa quem escreve
livre de saber qual banco está embaixo.
A classe não se declara em código de aplicação
A primeira surpresa é que a implementação não é algo que se abra para ler
em código de aplicação — nenhuma declaração de classe chamada
FWAdapterBaseV2 aparece em fonte de aplicação. Ela vem de mais fundo no
framework, e tudo que este artigo afirma vem de como o código de aplicação
a chama: quem escreve uma consulta paginada em TLPP herda dela com
From FWAdapterBaseV2.
_Super:New abre a herança, com underscore
Toda classe que herda chama o construtor da base no próprio New(), e a
grafia trava quem vem de outra linguagem: é _Super, com underscore — a
forma sem ele não é reconhecida. O construtor recebe o verbo HTTP e,
opcionalmente, um lógico. O anexo segue essa forma:
Method New() Class FornecedoresData
_Super:New("GET", .T.)
Return SelfO nome real do método de mapa é AddMapFields
É aqui que material de curso mais erra: nem SetFieldMap, nem
AddMapField no singular existem. O nome real é AddMapFields, no plural.
Os demais métodos do adapter que aparecem com frequência em código real:
Execute, FillGetResponse, SetQuery, SetWhere e SetOrder — os
cinco praticamente sempre presentes juntos —, seguidos por SetPage e
SetPageSize, que configuram a paginação em si. SetUrlFilter,
SetFields, SetUseSpaces, GetJSONResponse, SetOrderQuery,
DeActivate, GetCode e GetMessage completam o vocabulário, cada um
resolvendo um caso mais específico — o pacote não expõe a assinatura
completa de todos eles, fica registrado que existem e são usados, não mais
que isso.
AddMapFields tem uma assinatura observada de cinco posições fixas mais
uma opcional:
| posição | parâmetro | o que é |
|---|---|---|
| 1 | cPropriedadeJson | nome da propriedade no JSON de resposta |
| 2 | cCampoSQL | campo lido na consulta SQL |
| 3 | lRetorna | se o campo volta no envelope |
| 4 | lChave | se o campo é (parte da) chave |
| 5 | aEstrutura | {nome, tipo, tamanho, decimais} |
| 6 (opcional) | cExpressaoSQL | expressão SQL no lugar de um campo simples |
A sexta posição não é caso raro: é comum trocar um campo simples por um agregado, como neste total somado:
oSelf:AddMapFields('totalInsurance', 'TOTALINSUR', .T., .F., ;
{'TOTALINSUR', 'N', TamSX3('C8_SEGURO')[1], 0}, 'SUM(C8_SEGURO)')O anexo usa a forma comum, sem a sexta posição: cinco chamadas, uma por
campo da SA2, mapeando A2_COD, A2_LOJA, A2_NOME, A2_NREDUZ e
A2_CGC para propriedades em camelCase. Só codigo e loja levam .T. no
quarto parâmetro — é a chave composta da SA2.
Os coringas são #QueryFields# e #QueryWhere#
A consulta que alimenta a paginação não é montada campo a campo: é uma
string de SQL com dois pontos de substituição, sempre entre cerquilhas —
#QueryFields# e #QueryWhere#. A forma com sinal de porcentagem,
%QUERY_FIELDS%, não é reconhecida, embora circule em material de curso.
Também não existe um terceiro coringa para ordenação: ordenar é trabalho de
SetOrder(), fora da string SQL.
cQuery := "SELECT #QueryFields# FROM " + RetSqlName("SA2") + " SA2 "
cQuery += " WHERE #QueryWhere# "O framework troca #QueryFields# pela lista que AddMapFields mapeou e
#QueryWhere# pelo filtro de SetWhere — no anexo, a filial da SA2 e o
D_E_L_E_T_ de sempre, que uma API não dispensa por estar em HTTP.
A sequência obrigatória
Depois de configurar consulta, filtro, ordenação e página, o método segue
uma ordem fixa: Execute() e, só se devolver .T., FillGetResponse() —
sempre sem argumento — e por fim GetJSONResponse(), que devolve o JSON
pronto para oRest:setResponse(). GetResponse(), sem o Fill, não faz
parte desse fluxo, e é o erro mais comum de quem chega vindo do
WSRESTFUL antigo, que tem outro vocabulário de método.
O envelope que os componentes de listagem esperam
GetJSONResponse() devolve um objeto com três chaves fixas: items,
hasNext e remainingRecords — formato estável o bastante para virar
contrato assumido por quem consome. remainingRecords também aparece
reimplementado à mão em código que faz a própria conta por fora da classe:
nTotal - (nPage * nPageSize), quantos registros ainda faltam depois da
página atual.
É esse formato que componentes de listagem prontos esperam para paginar
sozinhos, sem tratamento extra no front: a tela pede a próxima página
quando hasNext vem verdadeiro, e para quando vem falso. É a ponte entre
o “carregar mais resultados” que o usuário vê e o
SetPage()/SetPageSize() que o fonte chamou antes.
A classe que herda não é a classe que responde
Falta o fato que decide a forma do seu fonte, e ele não aparece em material
de curso nenhum: herança do adapter e annotation de verbo não convivem
na mesma classe. Quem declara From FWAdapterBaseV2 não tem @Get,
@Post ou qualquer outro verbo. Quem tem annotation não herda o adapter. A
interseção é vazia.
A separação é de responsabilidade: o controller anotado recebe a requisição
e devolve a resposta; a classe que herda o adapter consulta o banco e não
sabe que existe HTTP. Juntar as duas compila, e é tentador quando se quer um
fonte curto — mas não tem precedente no material da TOTVS, e o custo é
concreto: :: passa a alcançar os métodos herdados enquanto oRest:
alcança o objeto injetado, as duas sintaxes convivendo linha a linha no
mesmo método. É onde se erra.
O anexo, de ponta a ponta
pgv.api.fornecedores.tlpp segue exatamente essa separação, em duas classes.
FornecedoresApi é o controller anotado, sem herança nenhuma.
FornecedoresData, que herda o adapter, concentra a consulta: mapeia os
campos com AddMapFields, monta a SQL com os dois coringas, chama
Execute() e, no caminho de sucesso, FillGetResponse(). O controller pede
GetJSONResponse() e devolve. PostFornecedores() e DeleteFornecedores()
já saem do que o adapter resolve — gravam pelo ExecAuto, porque ele
abstrai leitura paginada, não gravação. É fonte didático do site, escrito
para este artigo.
Antes de reaproveitar o mapa de campos, confira a estrutura real da SA2 no seu ambiente:
SELECT A2_COD, A2_LOJA, A2_NOME, A2_NREDUZ, A2_CGC, A2_MSBLQL
FROM SA2010
WHERE D_E_L_E_T_ = ' 'Se sua API TLPP pagina errado, ou não pagina, fale com a gente.
Anexos
#include "tlpp-core.th"
#include "tlpp-rest.th"
/*
Exemplo didatico do site Pragiva. Le e grava na SA2 (fornecedores) do
dicionario padrao. Precisa do servico REST configurado no ambiente.
DUAS CLASSES, de proposito. Heranca do adapter e annotation de verbo nao
convivem na mesma classe: o controller anotado responde a HTTP; a consulta
paginada herda o adapter e nao sabe que HTTP existe. Juntar as duas compila,
mas inverte a separacao que o framework pressupoe.
Arquivo unico com duas classes e recurso raro. Aqui existe para o exemplo
caber num anexo; num projeto real, um arquivo por classe.
Rotas:
GET /api/pragiva/v1/fornecedores -> listagem paginada
POST /api/pragiva/v1/fornecedores -> inclusao pelo ExecAuto
DELETE /api/pragiva/v1/fornecedores -> exclusao pelo ExecAuto
*/
namespace pragiva.api.fornecedores
// O CONTROLLER. Nao herda nada: quem responde pelas rotas e ele, e o objeto
// da requisicao chega injetado pelo runtime da annotation.
Class FornecedoresApi
Public Method New()
@Get("/api/pragiva/v1/fornecedores")
Public Method GetFornecedores()
@Post("/api/pragiva/v1/fornecedores")
Public Method PostFornecedores()
@Delete("/api/pragiva/v1/fornecedores")
Public Method DeleteFornecedores()
Private Method Responder(nCodigo, cCorpo)
Private Method ResponderErro(nCodigo, cMensagem)
Private Method ErroDoExecAuto()
EndClass
// A CONSULTA. Esta sim herda o adapter, e nao tem annotation nenhuma.
Class FornecedoresData From FWAdapterBaseV2
Public Method New()
Public Method BuscarPagina(nPagina, nPorPag)
EndClass
Method New() Class FornecedoresApi
Return Self
// Listagem paginada. O controller nao sabe paginar: instancia a classe de
// consulta e devolve o JSON que ela montou.
//
// "::" e apelido de "Self:", entao serve para os metodos DESTA classe. O
// objeto da requisicao nao e da classe: e injetado, e so atende por "oRest:".
Method GetFornecedores() Class FornecedoresApi
Local oDados as Object
Local jQuery as Json
Local nPagina as Numeric
Local nPorPag as Numeric
try
jQuery := oRest:getQueryRequest()
// Uma chamada sem query string nenhuma e o caso mais comum aqui, e
// nessa situacao o retorno pode nao ser objeto -- por isso o teste
// contra Nil antes de qualquer metodo.
// getQueryRequest() devolve Json; GetJsonText() de chave ausente
// volta a string literal 'null', nao Nil -- por isso o teste e
// contra essa string, com Val() convertendo para numero.
nPagina := 1
nPorPag := 10
If jQuery != Nil .And. jQuery:GetJsonText("page") != "null"
nPagina := Val(jQuery:GetJsonText("page"))
EndIf
If jQuery != Nil .And. jQuery:GetJsonText("pageSize") != "null"
nPorPag := Val(jQuery:GetJsonText("pageSize"))
EndIf
oDados := FornecedoresData():New()
If oDados:BuscarPagina(nPagina, nPorPag)
// O envelope tem items, hasNext e remainingRecords -- e esse
// formato que o componente de lista do PO UI espera para
// paginar sozinho, sem tratamento a mais no front.
::Responder(200, oDados:GetJSONResponse())
Else
::ResponderErro(oDados:GetCode(), oDados:GetMessage())
EndIf
catch oErro
::ResponderErro(500, oErro:Description)
endtry
Return
// Inclusao pelo ExecAuto, operacao 3. O que derruba quem migra do
// WSRESTFUL: getBodyRequest() devolve Character, nao Json pronto.
Method PostFornecedores() Class FornecedoresApi
Local jCorpo as Json
Local aDados as Array
Local cErro as Character
// lMsErroAuto e Private, nao Local: o ExecAuto grava o resultado nela
// por escopo dinamico, e uma declaracao Local fica invisivel para quem
// esta mais fundo na pilha. Vai ANTES do try, junto das demais
// declaracoes -- Private dentro de try nao e forma reconhecida.
Private lMsErroAuto := .F.
try
jCorpo := JsonObject():New()
// FromJson DEVOLVE o erro de parse, e descartar esse retorno e o
// buraco silencioso desta rota: com body malformado o objeto fica
// vazio, cada campo vira Nil, o ExecAuto nao acusa nada -- e a API
// responde 201 "incluido" sem ter gravado uma linha.
cErro := jCorpo:FromJson(oRest:getBodyRequest())
If !Empty(cErro)
::ResponderErro(400, "corpo da requisicao nao e um JSON valido")
Return
EndIf
If Empty(jCorpo["codigo"]) .Or. Empty(jCorpo["loja"]) .Or. ;
Empty(jCorpo["nome"])
::ResponderErro(400, "informe codigo, loja e nome")
Return
EndIf
aDados := {}
Aadd(aDados, {"A2_FILIAL", xFilial("SA2")})
Aadd(aDados, {"A2_COD" , jCorpo["codigo"]})
Aadd(aDados, {"A2_LOJA" , jCorpo["loja"]})
Aadd(aDados, {"A2_NOME" , jCorpo["nome"]})
Aadd(aDados, {"A2_NREDUZ", jCorpo["nomeReduzido"]})
Aadd(aDados, {"A2_CGC" , jCorpo["cnpjCpf"]})
MsExecAuto({|x, y| MATA020(x, y)}, aDados, 3)
If lMsErroAuto
::ResponderErro(400, ::ErroDoExecAuto())
Else
::Responder(201, '{"mensagem":"fornecedor incluido"}')
EndIf
catch oErro
::ResponderErro(500, oErro:Description)
endtry
Return
// Exclusao pelo ExecAuto, operacao 5. Quem se posiciona no registro e o
// proprio MATA020: para a operacao 5 o framework monta a chave a partir do
// ARRAY recebido e faz o seek sozinho. O DbSeek local existe por outro
// motivo -- responder 404 antes de entrar no ExecAuto.
Method DeleteFornecedores() Class FornecedoresApi
Local jQuery as Json
Local cCodigo as Character
Local cLoja as Character
Local aDados as Array
Local aArea as Array
Private lMsErroAuto := .F.
// A thread REST e reaproveitada entre requisicoes: a area de trabalho
// que este metodo posiciona sobrevive a ele. Guardar e restaurar impede
// que a proxima requisicao receba a SA2 parada onde esta parou.
aArea := GetArea()
try
jQuery := oRest:getQueryRequest()
cCodigo := jQuery:GetJsonText("codigo")
cLoja := jQuery:GetJsonText("loja")
// Parametro ausente vira a string 'null'. Sem esta guarda, a
// concatenacao da chave estoura e o cliente recebe 500 onde 400
// descreve o problema de verdade.
If Empty(cCodigo) .Or. cCodigo == "null" .Or. ;
Empty(cLoja) .Or. cLoja == "null"
::ResponderErro(400, "informe codigo e loja")
Return
EndIf
// PadR NAO e detalhe de estilo aqui: DbSeek casa por PREFIXO. Um
// codigo mais curto que o campo para no primeiro registro que
// COMECA com aquele valor -- e, numa exclusao, isso apaga o
// fornecedor errado sem nenhum aviso.
cCodigo := PadR(cCodigo, TamSX3("A2_COD")[1])
cLoja := PadR(cLoja, TamSX3("A2_LOJA")[1])
DbSelectArea("SA2")
SA2->(DbSetOrder(1)) // A2_FILIAL + A2_COD + A2_LOJA
SA2->(DbSeek(xFilial("SA2") + cCodigo + cLoja))
If !SA2->(Found())
::ResponderErro(404, "fornecedor nao encontrado")
Return
EndIf
aDados := {}
Aadd(aDados, {"A2_FILIAL", xFilial("SA2")})
Aadd(aDados, {"A2_COD" , cCodigo})
Aadd(aDados, {"A2_LOJA" , cLoja})
MsExecAuto({|x, y| MATA020(x, y)}, aDados, 5)
If lMsErroAuto
::ResponderErro(400, ::ErroDoExecAuto())
Else
::Responder(200, '{"mensagem":"fornecedor excluido"}')
EndIf
catch oErro
::ResponderErro(500, oErro:Description)
endtry
RestArea(aArea)
Return
// Um lugar so para o par setStatusCode + setResponse.
Method Responder(nCodigo, cCorpo) Class FornecedoresApi
oRest:setKeyHeaderResponse("Content-Type", "application/json")
oRest:setStatusCode(nCodigo)
oRest:setResponse(cCorpo)
Return
// O corpo do erro vai montado como OBJETO e serializado com ToJson: a
// mensagem de um erro real carrega aspas duplas, barra invertida e quebra
// de linha, e qualquer um dos tres invalida um JSON concatenado a mao.
Method ResponderErro(nCodigo, cMensagem) Class FornecedoresApi
Local jErro := JsonObject():New() as Json
jErro["erro"] := cMensagem
::Responder(nCodigo, jErro:ToJson())
Return
// O log do ExecAuto vem como array de linhas.
Method ErroDoExecAuto() Class FornecedoresApi
Local aLog := GetAutoGRLog() as Array
Local cTexto := "" as Character
Local nLinha := 0 as Numeric
For nLinha := 1 To Len(aLog)
cTexto += aLog[nLinha] + " "
Next nLinha
Return AllTrim(cTexto)
// ---------------------------------------------------------------------
// A classe de consulta. Herda o adapter e NAO tem annotation -- e assim
// que as duas responsabilidades ficam separadas.
// ---------------------------------------------------------------------
// _Super com underscore: e a unica grafia que o compilador reconhece.
Method New() Class FornecedoresData
_Super:New("GET", .T.)
Return Self
Method BuscarPagina(nPagina, nPorPag) Class FornecedoresData
Local cQuery as Character
Local cWhere as Character
// Os coringas sao #QueryFields# e #QueryWhere#, entre cerquilhas --
// %QUERY_FIELDS% e %QUERY_WHERE% nao existem no framework.
cQuery := "SELECT #QueryFields# FROM " + RetSqlName("SA2") + " SA2 "
cQuery += " WHERE #QueryWhere# "
cWhere := " SA2.A2_FILIAL = " + ValToSQL(xFilial("SA2")) + ;
" AND SA2.D_E_L_E_T_ = ' ' "
// AddMapFields, no plural -- SetFieldMap e AddMapField (singular) nao
// existem. O 5o parametro e sempre {nome, tipo, tamanho, decimais};
// TamSX3() devolve {tamanho, decimais, tipo}, entao o tamanho e o [1].
::AddMapFields("codigo" , "A2_COD" , .T., .T., {"A2_COD" , "C", TamSX3("A2_COD")[1] , 0})
::AddMapFields("loja" , "A2_LOJA" , .T., .T., {"A2_LOJA" , "C", TamSX3("A2_LOJA")[1] , 0})
::AddMapFields("nome" , "A2_NOME" , .T., .F., {"A2_NOME" , "C", TamSX3("A2_NOME")[1] , 0})
::AddMapFields("nomeReduzido", "A2_NREDUZ", .T., .F., {"A2_NREDUZ", "C", TamSX3("A2_NREDUZ")[1], 0})
::AddMapFields("cnpjCpf" , "A2_CGC" , .T., .F., {"A2_CGC" , "C", TamSX3("A2_CGC")[1] , 0})
::SetQuery(cQuery)
::SetWhere(cWhere)
::SetOrder("A2_FILIAL, A2_COD, A2_LOJA")
::SetPage(nPagina)
::SetPageSize(nPorPag)
If !::Execute()
Return .F.
EndIf
// A sequencia e Execute() -> FillGetResponse() -> GetJSONResponse().
// GetResponse() nao existe nesse fluxo. FillGetResponse() nao recebe
// parametro nenhum.
::FillGetResponse()
Return .T.Referências
TDNtdn.totvs.com
TDN — 09. FWAdapterBaseV2A classe é consumida por composição/herança em código de aplicação; a implementação em si não é código de aplicação legível.
Continue por aqui
- Vídeo
ProtheusTutorial
Query parameters e filtro dinâmico na API: o parâmetro que a rota não conhece
Query parameters não se declaram na rota nem aparecem na annotation, ao contrário do path param: filtram, ordenam e paginam, lidos por getQueryRequest.