Pragiva — Tecnologia na prática

GetArea e RestArea em AdvPL: área de trabalho e posicionamento

GetArea e RestArea em AdvPL: o que é a área de trabalho, por que qualificar todo acesso com ALIAS-> e por que o PE tem que devolver a área como recebeu.

Desenvolvimento Tutorial 4 min de leitura

Assista o vídeo

Resposta rápida

O que é salvo
alias, ordem do índice e número do registro (Alias(), IndexOrd(), RecNo()).
Área corrente
GetArea().
Área de uma tabela
SA1->(GetArea()).
Restaurar
RestArea(aArea), na ordem inversa em que as áreas foram salvas.
Onde é obrigatório
ponto de entrada e qualquer função chamada no meio de uma rotina padrão.
Alternativa do framework
FWGetArea() / FWRestArea(), com a mesma forma de chamada.
Nesta página7 seções

Quase todo erro de “o pedido gravou com o cliente errado” sem rastro no log começa aqui: uma customização mexeu no posicionamento de uma tabela e não devolveu. O código compila, roda, e o padrão continua trabalhando em cima do registro que você deixou.

Área de trabalho, em uma frase

Cada tabela aberta ocupa uma área de trabalho, identificada pelo alias (, ), com três coisas próprias: o índice ativo, o registro posicionado e os campos desse registro. Existe sempre uma área corrente — a última selecionada — e é ela que responde a todo comando escrito sem alias: EoF(), DbSkip(), RecNo() e um campo solto como perguntam para quem estiver selecionado naquele instante, não para a tabela que você tinha em mente.

Qualifique sempre: ALIAS->

Dois laços aninhados mostram o problema. O de fora percorre pedidos; o de dentro, os itens. Sem alias, o DbSkip() do laço externo roda depois do interno — e a área corrente, a essa altura, é a dos itens:

ADVPL / TLPP
// Frágil: EoF() e DbSkip() dependem de quem está selecionado
DbSelectArea("SC5")
While !EoF()
    DbSelectArea("SC6")
 //... percorre os itens...
    DbSkip()   // ainda está na SC6
EndDo

A correção que resiste a manutenção é dizer a tabela em cada acesso:

ADVPL / TLPP
While SC5->(!EoF())
 While SC6->(!EoF()) .And. SC6->C6_NUM == SC5->C5_NUM
 //...
        SC6->(DbSkip())
    EndDo
    SC5->(DbSkip())
EndDo

SC5->(expressão) avalia a expressão com a SC5 como área corrente e devolve a seleção anterior no fim. Espalhar DbSelectArea() antes de cada comando também funciona — até alguém inserir uma linha no meio. SA1->A1_NOME, com alias, lê o campo da SA1 independente de quem esteja selecionado.

O laço em si está em Condições e laços em AdvPL.

Posicionar: ordem, filial, chave

Posicionar é sempre a mesma sequência: escolher o índice e buscar pela chave completa, começando pela filial.

ADVPL / TLPP
SA1->(DbSetOrder(1))   // A1_FILIAL+A1_COD+A1_LOJA
If SA1->(DbSeek(xFilial("SA1") + cCodCli + cLojaCli))
    // SA1 posicionada no cliente
EndIf

No dicionário, a ordem 1 da SA1 é A1_FILIAL+A1_COD+A1_LOJA e a 2 é A1_FILIAL+A1_NOME+A1_LOJA; na SB1, a 1 é B1_FILIAL+B1_COD. Confira no seu ambiente antes de fixar um número no código:

Confira no seu ambienteSQL
SELECT INDICE, ORDEM, CHAVE, DESCRICAO
FROM SIX010
WHERE INDICE IN ('SA1', 'SB1') AND D_E_L_E_T_ = ' '
ORDER BY INDICE, ORDEM

xFilial("SA1") devolve a filial no formato que a tabela espera — compartilhada ou exclusiva. Montar a chave com cFilAnt direto funciona até mudar o compartilhamento da tabela. A assinatura da função nativa é xFilial( cAlias, cFil ), e é ela quem decide esse formato por você.

DbSeek que não encontra deixa a tabela em fim de arquivo. Por isso o teste do retorno não é opcional: sem ele, o código seguinte lê campos vazios sem erro.

O que GetArea() guarda

GetArea() é enxuta por definição: devolve um array { Alias(), IndexOrd(), RecNo() }. Três posições, nada mais — não guarda filtro, não guarda campos, não guarda a seleção de outras tabelas.

RestArea(aArea) faz o caminho de volta: DbSelectArea no alias, DbSetOrder se a ordem mudou e DbGoTo se o registro mudou. Dois comportamentos que a documentação oficial não deixa claros, e que só aparecem na prática:

  • se a área já foi fechada (Select() igual a zero), ele não faz nada — não dá erro, também não restaura;
  • GetArea(cSqlAlias), com o alias igual ao corrente, grava Nil no lugar do registro, e o RestArea correspondente não reposiciona. É um caso de tabela em inclusão, não o uso comum.

A diferença entre as duas formas de chamar:

ChamadaSalva
GetArea()a área corrente, seja qual for
SA1->(GetArea())a área da SA1, e só ela

GetArea() sozinho não protege a SA1 se a corrente era a SC5. Para cada tabela em que você vai mexer, um ALIAS->(GetArea()).

Por que em ponto de entrada é obrigatório

O ponto de entrada roda no meio da rotina padrão. Antes de chamar você, a rotina padrão já posicionou as tabelas de que precisa; depois que você devolve o controle, ela continua lendo essas tabelas sem reposicionar. Um DbSeek seu na SA1, e na linha seguinte do padrão SA1->A1_... é o cliente que você buscou. O mecanismo da chamada está em Ponto de entrada no Protheus.

Exemplo

Um ponto de entrada fictício de validação do pedido, PGV001, que confere se algum produto do grid está bloqueado e se o cliente tem risco E.

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

/*/{Protheus.doc} PGV001
Validação fictícia do pedido: recusa cliente de risco E e produto bloqueado.
Devolve as áreas exatamente como recebeu.
@type user function
@author Pragiva
@return logical, .T. permite continuar
/*/
User Function PGV001()

    Local aArea    := GetArea()          // corrente: salva primeiro
    Local aAreaSA1 := SA1->(GetArea())
    Local aAreaSB1 := SB1->(GetArea())
 Local lOk := .T.
    Local nPosPrd  := 0
    Local nX       := 0

    SA1->(DbSetOrder(1))
    If SA1->(DbSeek(xFilial("SA1") + M->C5_CLIENTE + M->C5_LOJACLI))
        lOk := SA1->A1_RISCO <> "E"
    EndIf

 If lOk .And. Type("aCols") == "A" .And. Type("aHeader") == "A"
        nPosPrd := GDFieldPos("C6_PRODUTO", aHeader)
        SB1->(DbSetOrder(1))
        For nX := 1 To Len(aCols)
            If SB1->(DbSeek(xFilial("SB1") + aCols[nX, nPosPrd])) ;
                    .And. SB1->B1_MSBLQL == "1"
 lOk := .F.
                Exit
            EndIf
        Next nX
    EndIf

    // Ordem inversa: a corrente volta por último
    RestArea(aAreaSB1)
    RestArea(aAreaSA1)
    RestArea(aArea)

Return lOk

A ordem inversa não é estética: cada RestArea termina com um DbSelectArea, e quem é restaurado por último define a área corrente. Restaurar aArea antes da SB1 deixaria a SB1 selecionada — o padrão não esperava isso.

Type("aCols") guarda contra variável Private ausente em alguma chamada — veja Escopo de variáveis em AdvPL. Dado vindo do PARAMIXB tem cuidado próprio, em PARAMIXB, o array que às vezes não é seu.

E FWGetArea / FWRestArea?

Existem, e o próprio padrão as usa com a mesma forma de chamada — salvando RD0->(FwGetArea()) e FW5->(FwGetArea()), por exemplo, e restaurando com FWRestArea, na ordem inversa. A definição não é pública, então não há como detalhar o que guardam além do uso observado. Para código novo, são a opção do framework; GetArea/RestArea continua funcionando e é o que você vai encontrar em quase todo legado.

Quando a consulta é só leitura e não precisa do registro posicionado, a saída mais limpa é nem tocar na área: ler por SQL — Consultas SQL em AdvPL.


Se uma customização sua está trocando registro em silêncio e você quer uma segunda leitura, fale com a gente.

Referências

  1. TDNtdn.totvs.com

    TDN — DBSelectArea
  2. TDNtdn.totvs.com

    TDN — DBSeek
  3. TDNtdn.totvs.com

    TDN — DBGoTo

Continue por aqui