Pragiva — Tecnologia na prática

Paginação com FWAdapterBaseV2: a classe que abstrai o banco

FWAdapterBaseV2 abstrai a sintaxe de paginação de SQL Server, Oracle e Postgres. AddMapFields, não SetFieldMap, é o método que mapeia cada campo.

Protheus Tutorial 5 min de leitura

Assista o vídeo

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; FWAdapterBaseV2 faz 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 — SetFieldMap e AddMapField, 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:

ADVPL / TLPP
Method New() Class FornecedoresData
 _Super:New("GET", .T.)
Return Self

O 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çãoparâmetroo que é
1cPropriedadeJsonnome da propriedade no JSON de resposta
2cCampoSQLcampo lido na consulta SQL
3lRetornase o campo volta no envelope
4lChavese o campo é (parte da) chave
5aEstrutura{nome, tipo, tamanho, decimais}
6 (opcional)cExpressaoSQLexpressã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:

ADVPL / TLPP
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 , , , e 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.

ADVPL / TLPP
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:

Confira no seu ambienteSQL
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

pgv.api.fornecedores.tlpp
API REST de fornecedores em TLPP: o controller anotado numa classe e a consulta paginada, que herda FWAdapterBaseV2, em outra. Exemplo didatico sobre a SA2 padrao.
Baixar fonte
pgv.api.fornecedores.tlppADVPL / TLPP
#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

  1. 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