CRUD em AdvPL – Parte 03

Introdução

Nos posts anteriores, vimos um programa de exemplo de Agenda em AdvPL, com uma interface única de cadastro e consulta, usando um Banco de Dados acessado via DBAccess, com vários trechos de código explicados. Agora, vamos ver alguns detalhes importantes, e possíveis pontos de falha e melhoria.

Tamanho dos objetos GET

Somente ao baixar o fonte completo, você descobre que a chamada de função CALCSIZEGET(), usada na criação de todos os objetos GET da Interface na verdade não é uma função. Vejamos o código:

@ 5,60 GET oGet1 VAR cID PICTURE "@!" SIZE CALCSIZEGET(6) ,12 OF oPanelCrud PIXEL
@ 20,60 GET oGet2 VAR cNome PICTURE "@!" SIZE CALCSIZEGET(50),12 OF oPanelCrud PIXEL
@ 35,60 GET oGet3 VAR cEnder PICTURE "@!" SIZE CALCSIZEGET(50),12 OF oPanelCrud PIXEL
@ 50,60 GET oGet4 VAR cCompl PICTURE "@!" SIZE CALCSIZEGET(20),12 OF oPanelCrud PIXEL
@ 65,60 GET oGet5 VAR cBairro PICTURE "@!" SIZE CALCSIZEGET(30),12 OF oPanelCrud PIXEL
@ 80,60 GET oGet6 VAR cCidade PICTURE "@!" SIZE CALCSIZEGET(40),12 OF oPanelCrud PIXEL
@ 95,60 GET oGet7 VAR cUF PICTURE "!!" SIZE CALCSIZEGET(2) ,12 OF oPanelCrud PIXEL
@ 110,60 GET oGet8 VAR cCEP PICTURE "@R 99999-999" SIZE CALCSIZEGET(9),12 OF oPanelCrud PIXEL

Reparem que, para calcular o comprimento de cada objeto GET na tela, foi usada aparentemente uma função, que recebeu como parâmetro o tamanho máximo de cada informação. Na verdade, CALCSIZEGET() não é uma função, mas sim uma diretiva de compilação do AdvPL, case-sensitive, que permite a passagem de parâmetro, e na verdade é substituída durante a pré-compilação do código. Esta diretiva está declarada nas primeiras linhas do código-fonte:

#define CALCSIZEGET( X ) (( X * 4 ) + 4)

Na prática, o que você especificar como “X” para a pseudo-função aicma, será substituído na expressão resultante. Por exemplo, veja o código abaixo:

Conout( CALCSIZEGET(10) )

Este trecho de código, durante a pré-compilação do AdvPL — normalmente realizada no IDE / TDS — vai ser transformada na linha abaixo:

Conout( (( 10 * 4 ) + 4 ) )

Como a expressão resultante somente têm constantes, a compilação do AdvPL deve optimizar esse código, calculando o resultado final no momento da compilação efetiva do código no Protheus Server, sendo que o resultado final dentro do servidor de aplicação será:

Conout( 44 )

Sobre esta escolha, temos os seguintes pontos a considerar:

  1. Por hora não estava previsto mexer no tamanho dos campos da tabela de agenda, e sendo que a expressão usa constantes, além de não haver uma função sendo chamada, os valores são resolvidos na compilação.
  2. Como não estava previsto mexer no tamanho dos campos da tabela da Agenda, não tem problema nenhum fazer algo assim. Porém, caso o campo seja alterado, deve-se também alterar essa parte do código, senão o conteúdo do campo pode não caber dentro do objeto.
  3. Eu parti da premissa de usar uma fonte de tela do tipo “mono-espaçada”, isto é, cada caractere possui o mesmo tamanho. Escolhi Courier New, tamanho 16. Caso você troque a fonte para outro tamanho, ou mesmo outro tipo — como o caso de fontes True-Type (cada letra ou símbolo pode ter tamanho diferente) —  esta fórmula precisa ser reconsiderada, partindo da premissa que o objeto GET deve permitir a representação do tamanho máximo de caracteres, usando a maior letra da fonte (normalmente M ou W) .
  4. Caso o tamanho dos campos seja dinâmico, ainda assim poderíamos usar a pseudo-função,  pois ela é relativamente simples, e mesmo que o fato de usar uma variável não a torne candidata para uma optimização no nível da compilação, ela terá menos “overhead” do que uma função com a mesma finalidade, apenas pelo fato da execução não precisar criar um stack e um contexto para a execução da função, afinal as operações matemáticas serão realizadas no próprio contexto atual, para a passagem de parâmetros para a classe TGET.

Acesso a Dados

Em nenhum momento, exceto para ordenação de consulta e para a geração de um novo ID automaticamente para a inclusão de um novo registro da Agenda, o acesso a dados fez uso de busca usando índice, filtro ou mesmo Queries no Banco de Dados. Bem, na prática, ainda não era o momento de chegar “tão fundo”. Porém, também temos algumas coisas a considerar sobre as manutenções dos registros nesta primeira versão do fonte:

Acesso a Dados – Inclusões concorrentes

Embora o programa abra a tabela de Agenda em modo compartilhado, e mais de uma instância do Smart Client possa ser executada para consultar ou mesmo dar manutenção nos dados da agenda. A primeira situação de concorrência não tratada adequadamente é a geração de um novo ID automaticamente.

Caso dois terminais entrem ao mesmo tempo na opção de inclusão, ambos trocam a ordem ativa da tabela para ordem de código, posicionam no último registro, pegam o valor do último registro e somam uma unidade. Logo, ambos vão abrir uma interface para preenchimento dos dados do campo, e se ambos confirmarem a operação de inclusão, dois registros da agenda serão gravados com o mesmo ID.

Na prática, isso ainda é muito ruim, pois se a premissa da agenda é não der nossos nomes em duplicidade com IDs diferentes, é boa prática criar um índice único para tabelas de sistema, para que caso uma regra da chave única seja violada — como não permitir dois IDs com a mesma chave.

Na minha opinião, pode até ser interessante mostrar o novo valor antes mesmo de ter os dados para inclusão, mas uma alternativa simples e mais segura neste caso seria esconder o campo ID da tela,  e apenas criar um novo ID no momento de gravar efetivamente a informação, ela é irrelevante no momento do cadastro — posteriormente o programa será alterado com estas funcionalidades e postado novamente na continuação dessa sequência sobre CRUD.

Acesso a Dados – Alterações Concorrentes

Um ponto importante, que embora tenha sido tratado no fonte, ainda está sujeito a erros, é o bloqueio do registro para alteração. Imagine que dois usuários acessem o mesmo registro da agenda, e ambos abram a tela de alteração. Após cada um dos usuários alterar alguns valores do cadastro, ambos puderam editar os dados do registro consultado sem fazer nenhum tipo de reserva ou bloqueio — pois o fonte foi escrito para trabalhar dessa forma — mas no momento de gravar a alteração, eu preciso solicitar um bloqueio do registro — feito pela função DBRLOCK() — então eu gravo as informações atualizadas e solto o bloqueio.

Se ambos os operadores alteraram dados do registro, sem que um saiba o que o outro alterou, o último usuário que confirmar a gravação vai passar por cima dos dados editados ou alterados pelo primeiro usuário.

Por outro lado, caso você opte por bloquear o registro no momento da abertura da interface, e somente desbloquear na gravação ou cancelamento da operação, nenhum outro programa conseguirá o bloqueio deste registro. Se o usuário que fez o bloqueio começou a alterar os dados, atendeu um telefonema e saiu correndo, enquanto ele não voltar e terminar a operação que começou, este registro permanecerá bloqueado.

Usando um mecanismo alternativo

Para cada problema existe uma saída que melhor se encaixa na necessidade do sistema e da aplicação. Quando pensamos em transações curtas e disponibilidade da informação, fazer um bloqueio de registro com a interface de edução ativa vai na contramão das boas práticas de desenvolvimento de sistemas escalares e resilientes.

Existem várias formas de contorno dessa situação, cada uma tem os seus prós e contras, a seguir:

  • Time-Out implementado em código

Utilizar o próprio mecanismo de bloqueio de registro na abertura da interface, porém utilizar um objeto de timer de interface (tTimer), habilitado para executar um bloco de código a cada 5 segundos, e implementar nos objetos GET um Reset() do timer a cada troca de foco de componente da interface. Desse modo você consegue ter um controle aproximado de quanto tempo a interface ficou sem uso na edição, e neste caso você pode usar o Timer() para abandonar a rotina caso o tempo seja excedido.

  • Time-Out implementado na Plataforma

Usando uma configuração chamada INACTIVETIMEOUT do Protheus, podemos programaticamente definir um tempo que a interface pode ficar sem interação. Porém, este tempo vale para a interface inteira, não apenas quando você está alterando um registro, e uma vez que o Time-Out aconteça, a aplicação é finalizada no ponto onde ela está em espera.

  • Bloqueio por mecanismo secundário

Ao invés de usar o bloqueio em disco, você pode criar um mecanismo de reserva de registro para alteração, por exemplo, gravado em uma tabela de dados auxiliar. A principal desvantagem é overhead de disco para incluir, alterar e excluir cada reserva de dados, e o bloqueio de registro com DBRLOCK() será necessário de qualquer forma,.

  • Atualização por diferença e resolução de conflitos

Este é o mecanismo mais elegante, pode dar um pouco de trabalho para implementar, mas definitivamente é o mais bonito. A aplicação guarda uma cópia do registro na memória logo que o processo de alteração de dados é iniciado, sem fazer o bloqueio do registro, e quando o usuário realmente terminar a alteração e confirmar, a rotina deve fazer o bloqueio do registro, e verificar se houve alteração — e em quais campos — enquanto o operador estava alterando os dados, bem como verificar se as alterações dos dados feitas me memória pelo usuário atual conflitam com as alterações feitas por outro usuário enquanto o mesmo estava editando os valores dos campos.

A resolução de conflitos seria algo mais ou menos assim:

  1. Somente é considerado que existe conflito nos dados de um campo, se este campo começou com um valor X, enquanto o operador A estava editando o registro, o operador B alterou e gravou a informação Y no valor do campo, e agora o operador A tenta gravar o valor Z no campo. Neste caso, o operador A iria sobrescrever a alteração feita pelo operador B, sem nunca ter visto que o valor do campo tinha sido alterado de X para Y.
  2. Se ambos os operadores trocaram o valor do campo para o mesmo valor final, não há conflito.
  3. O programa de gravação deve apenas gravar os valores dos campos alterados por aquele usuário, sob pena de sobrescrever os campos alterados por outro enquanto este mexia nos dados.

Acesso a Dados – Tabela sempre aberta e na conexão de interface

Outro ponto a ser explorado é que, como a tabela AGENDA é aberta junto da criação da janela do programa, e apenas fechada na saída ou fechamento da janela, e durante o processamento a aplicação somente se preocupa em fazer um checkup inicial na abertura da tabela e persistir / recuperar os dados. Partindo da premissa que um ponteiro de registro de uma tabela aberta pode ser movido para a frente ou para trás sob um determinado índice, a consulta mantém a tabela e a navegação abertas na ordem 2 (alfabética), e quando é necessário inserir um novo registro na agenda, a troca para ordem de código e a movimentação da tabela para o final do arquivo é feita apenas para descobrir qual é o maior e último ID cadastrado, para a partir dele criar um novo.

Conclusão

A proposta para este código é, após refatorado, ser desmembrado em dois processos, um para interface e outro para acesso a dados. Conforme for aparecendo a necessidade, vamos apertando os parafusos, até que a aplicação fique linda, e gere TERABYTES DE SUCESSO !!!

Referências

 

 

Anúncios

Deixe um comentário

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s