Pragiva — Tecnologia na prática

FWTemporaryTable: tabela temporária em AdvPL do jeito certo

FWTemporaryTable: como criar, preencher, cruzar em SQL e apagar uma tabela temporária em AdvPL, substituto do CriaTrab, na sequência que o padrão usa.

Desenvolvimento Tutorial 4 min de leitura

Assista o vídeo

Resposta rápida

O que é
uma classe do framework que cria uma tabela temporária no banco, com alias próprio.
Sequência
New(cAlias), SetFields(aStru), AddIndex(...), Create().
Em SQL
GetRealName() devolve o nome físico, para INSERT... SELECT e JOIN.
Em AdvPL
o alias funciona como qualquer tabela — RecLock, DbSeek, DbSkip.
Obrigatório
Delete() ao final. Sem isso, a tabela fica ocupando o banco.
Nesta página6 seções

Guardar um resultado intermediário em array funciona com cem linhas. Com cem mil, você reescreve à mão ordenação e busca que o banco faz de graça — veja o limite em arrays em AdvPL. A saída é uma tabela temporária, e no Protheus atual ela se chama FWTemporaryTable.

Por que não CriaTrab

Em fonte antigo você ainda vai encontrar CriaTrab — o próprio padrão chama, dentro de SelDados():

ADVPL / TLPP
cIndex := CriaTrab(nil,.f.)

Ele gerava um arquivo de trabalho fora do SGBD: não participa de query, e a limpeza dependia de quem escreveu lembrar de apagá-lo — um hábito fácil de esquecer no meio de uma rotina grande, com vários pontos de saída. A orientação da TOTVS para código novo é FWTemporaryTable, que cria a tabela dentro do banco — e abre a porta que importa: cruzar a temporária com SE1, SA1 ou qualquer outra tabela num único comando SQL.

Em legado, não troque CriaTrab por atacado: muitas dessas chamadas só geram um nome de arquivo de índice.

A sequência, lida na rotina padrão

A classe mora na biblioteca do framework, não em código-fonte visível diretamente. O jeito de provar como ela se usa é ler quem a usa. Dentro de Atf040Item(), a sequência completa aparece em ordem:

ADVPL / TLPP
cTrbTmp := GetNextAlias()

_oATFA040 := FWTemporaryTable():New( cTrbTmp )
_oATFA040:SetFields(aStruct)
_oATFA040:AddIndex("1", {"ATFITEM"})

_oATFA040:Create()
cTmpReal := _oATFA040:GetRealName()

O que cada linha diz:

MétodoO que faz
New(cAlias)Instancia a tabela e define o alias pelo qual o AdvPL vai acessá-la
SetFields(aStru)Recebe a estrutura: um array com {nome, tipo, tamanho, decimal} por campo
AddIndex(cOrdem, aCampos)Declara um índice; o primeiro argumento é o identificador, o segundo, os campos
Create()Cria de fato a tabela no banco — antes dele, nada existe
GetRealName()Devolve o nome físico, que é o que vai dentro do SQL
Delete()Apaga a tabela e libera o alias

Repare no GetNextAlias() — mas não generalize a partir dele. Em boa parte das chamadas de FWTemporaryTable():New(...) pelo padrão, o alias já chega literal — inclusive na rotina ATFA012, adiante, com New( "ATFCOE" ). Alias fixo colide quando a rotina roda duas vezes na mesma thread, ou quando outra customização teve a mesma ideia; o sintoma é um erro de tabela já aberta, no meio de uma rotina que funcionava até ontem. É por isso que aquela rotina, mais abaixo, testa se o objeto já existe antes de recriá-lo. GetNextAlias() evita o problema na raiz, sem precisar dessa defesa extra.

Montando a estrutura sem digitar tamanho

Digitar {"SALDO","N",16,2} funciona até o dia em que alguém aumenta o campo no dicionário. O padrão resolve lendo o tamanho do SX3: o setUp() da classe F811TmpFWP percorre a lista de campos e faz AAdd(::aStruct, FWSX3Util():getFieldStruct( allTrim(::aFieldList[nX]) )) em cada um — o retorno já vem no formato que o SetFields espera, com nome, tipo, tamanho e decimal nas posições 1 a 4.

Repare no que ele faz depois do laço: acrescenta {'FWP_OK','C',1,0} à mão. Campo de controle que não existe no dicionário não tem de onde ser lido, e é essa mistura — estrutura derivada do SX3 para os campos reais, literal para os de controle — que aparece no padrão.

A alternativa mais antiga é TamSX3(cCampo). Atenção à ordem: ela devolve {tamanho, decimal, tipo} — o tipo vem por último, ao contrário do que o SetFields espera. Misturar as duas ordens é um erro silencioso.

Exemplo: saldo em aberto por cliente

O padrão que aparece em código de produção: carregar a temporária com um INSERT... SELECT e depois percorrê-la em AdvPL, marcando registros.

PGV010.prwADVPL / TLPP
#Include "Protheus.ch"

/*/{Protheus.doc} PGV010
Monta, numa tabela temporária, o saldo em aberto por cliente e marca
os que têm título vencido.
@type user function
@author Pragiva
/*/
User Function PGV010()

    Local aStru   := {}
    Local cAlias  := GetNextAlias()
    Local oTmp    := Nil
    Local cTabela := ""
    Local cSql    := ""

    aAdd(aStru, CampoDe("CLIENTE", "E1_CLIENTE"))
    aAdd(aStru, CampoDe("LOJA",    "E1_LOJA"))
    aAdd(aStru, CampoDe("NOME",    "A1_NOME"))
    aAdd(aStru, CampoDe("SALDO",   "E1_SALDO"))
    aAdd(aStru, CampoDe("VENCTO",  "E1_VENCREA"))
    aAdd(aStru, {"VENCIDO", "C", 1, 0})

    oTmp := FWTemporaryTable():New(cAlias)
    oTmp:SetFields(aStru)
    oTmp:AddIndex("1", {"CLIENTE", "LOJA"})
    oTmp:Create()

    cTabela := oTmp:GetRealName()

    cSql := "INSERT INTO " + cTabela + " (CLIENTE, LOJA, NOME, SALDO, VENCTO, VENCIDO) "
    cSql += "SELECT E1_CLIENTE, E1_LOJA, A1_NOME, SUM(E1_SALDO), MIN(E1_VENCREA), 'N' "
    cSql += "FROM " + RetSqlName("SE1") + " SE1 "
    cSql += "INNER JOIN " + RetSqlName("SA1") + " SA1 "
    cSql += "   ON A1_FILIAL = '" + xFilial("SA1") + "' "
    cSql += "  AND A1_COD = E1_CLIENTE AND A1_LOJA = E1_LOJA "
    cSql += "  AND SA1.D_E_L_E_T_ = ' ' "
    cSql += "WHERE E1_FILIAL = '" + xFilial("SE1") + "' "
    cSql += "  AND E1_SALDO > 0 AND SE1.D_E_L_E_T_ = ' ' "
    cSql += "GROUP BY E1_CLIENTE, E1_LOJA, A1_NOME"

    If TcSqlExec(cSql) < 0
        ConOut("[PGV010] Falha ao carregar temporaria: " + TcSqlError())
    Else
        (cAlias)->(DbSetOrder(1))
        (cAlias)->(DbGoTop())

        While (cAlias)->(!Eof())
            If (cAlias)->VENCTO < dDataBase
 RecLock(cAlias, .F.)
                    (cAlias)->VENCIDO := "S"
                (cAlias)->(MsUnlock())
            EndIf
            (cAlias)->(DbSkip())
        EndDo
    EndIf

    // Consumo do resultado. Depois, sempre:
    oTmp:Delete()

Return Nil

Static Function CampoDe(cNome, cCampoSX3)
    Local aInfo := FWSX3Util():GetFieldStruct(cCampoSX3)
Return {cNome, aInfo[2], aInfo[3], aInfo[4]}

Três leituras.

O INSERT lista só as suas colunas. É o que o padrão faz dentro de F090MTTMP(), com GetRealName() e a lista de campos, seguido de TcSQLExec e do teste de retorno negativo. Carregar pelo banco evita trazer linha por linha para o application server.

Depois do INSERT, o alias enxerga os dados. DbSeek, DbSkip e RecLock funcionam no alias da temporária como em qualquer tabela — para ir direto a um cliente, (cAlias)->(DbSeek(cCliente + cLoja)) usa o índice “1” do AddIndex. RecLock é a escolha óbvia aqui, tabela sua e sem regra do padrão; o critério geral está em RecLock ou ExecAuto.

A data vai como tipo D. No banco ela é texto AAAAMMDD, por isso o MIN(E1_VENCREA) funciona e o AdvPL lê de volta como data.

Para a montagem da query em si — RetSqlName, xFilial, D_E_L_E_T_ e o cuidado com concatenação — veja consultas SQL em AdvPL.

O Delete não é opcional

Na rotina ATFA012, dentro de AF012aCalc() — o mesmo trecho do alias fixo "ATFCOE", lá em cima —, o padrão testa se o objeto já existe e chama Delete() antes de recriá-lo: defesa certa para rotina executada mais de uma vez na sessão. Com vários pontos de saída, centralize a limpeza no fim. É a mesma disciplina de GetArea e RestArea: o que você abriu, você fecha.

Onde ela compensa

  • Base de relatório ou browse de várias tabelas, entregue a um componente que espera alias.
  • Cruzamento em SQL entre uma lista sua — códigos vindos de planilha, por exemplo — e as tabelas do sistema, num JOIN só.

Para dez linhas, array continua sendo a ferramenta certa.


Se uma rotina sua está lenta por carregar tudo em array e você quer uma segunda leitura, fale com a gente.

Referências

  1. TDNtdn.totvs.com

    TDN — FWTemporaryTable

Continue por aqui