CRUD em AdvPL – Parte 05

Introdução

No post anterior, vimos algumas considerações sobre uso de MUTEX em AdvPL. Agora, vamos aplicar uma delas na Agenda, e ver algumas implementações interessantes, como índice único e a utilização dos índices para realizar buscas rápidas.

Novo ID para a Agenda

Devido ao porte do programa, e prevendo um cenário inicial onde apenas um servidor de aplicação Protheus seria o suficiente para vários usuários realizarem por exemplo operações de inclusão de dados concorrentemente, optei por usar um MUTEX com escopo apenas da instância atual do servidor de aplicação Protheus — GlbNmLock() e GlbNmUnlock(). Vamos ver abaixo como o fonte de criação de novo código ficou:

STATIC Function GetNewID()
Local cNewId
Local nRetry := 0
While !GlbNmLock("AGENDA_ID")
  nRetry++
  If nRetry > 20
    // 2 segundos sem conseguir lock ?
    // retorna uma string vazia
    return ""
  Endif
Sleep(100)
Enddo

// Se eu vou incluir, pega o Ultimo ID e soma 1
DBSelectArea("AGENDA")
DbsetOrder(1)
DBGobottom()

// Pega o valor do ultimo registro e soma 1
cNewId := StrZero( val(AGENDA->ID) + 1 , 6 )

// Solta o lock para outro processo poder pegar um novo ID
GlbNmUnlock("AGENDA_ID")

Return cNewId

Na prática, houve uma alteração no comportamento da função. Caso ela não consiga obter o bloqueio do identificador “AGENDA_ID”, a função espera 1/10 de segundo (100 ms) e tenta novamente, por até 20 vezes. Caso ela não consiga, ela apenas retorna uma string em branco. Esta mudança deve ser tratada em quem consome esta função, vide abaixo um trecho do novo código de confirmação da inserção de novo registro na Agenda:

cNewID := GetNewID()

// Release 1.2
// Se o ID está vazio, não foi possível obter o bloqueio nomeado
// para a geração do ID -- Muitas operações de inserção concorrentes

While empty(cNewID)
  If MsgInfo("Falha ao obter um novo ID para inclusão."+;
             "Deseja tentar novamente ?")
    cNewID := GetNewID()
    LOOP
  Endif 
  MsgStop("Não é possível incluir na agenda neste momento."+;
         "Tente novamente mais tarde.")
  Return
Enddo

Agora sim, com as alterações acima, podemos tratar concorrência entre inserções, dentro do mesmo servidor de aplicação, sem que um processo gere o mesmo ID de inclusão de outro processo. Agora, vamos deixar esse fonte mais ninja ainda ? Vamos criar no banco de dados um índice único, o que fará o próprio banco de dados recusar uma inserção de registro, caso seja usado um ID já existente na tabela.

Índice único no DBAccess

Usamos a função TCUnique() para criar um índice de chave única — ou índice único — no banco de dados para uma determinada tabela. Por default, o índice único é criado pelo DBAccess no Banco de Dados, usando o nome da tabela mais o sufixo “_UNQ”. Logo, para uma tabela chamada  “AGENDA”, o índice único correspondente será “AGENDA_UNQ”. Vamos ver como ficaria o fonte de abertura da tabela de agenda, logo após o teste da existência e criação do arquivo de dados.

If !tccanopen(cFile,cFile+'_UNQ')
  // Se o Indice único da tabela nao existe, cria 
  USE (cFile) ALIAS (cFile) EXCLUSIVE NEW VIA "TOPCONN"
  nRet := TCUnique(cFile,"ID")
  If nRet < 0 
    MsgStop(tcsqlerror(),"Falha ao criar índice único")
    QUIT
  Endif
  USE
EndIf

Com o índice único criado, experimente alterar a geração do ID, para não somar 1 , e usar o último ID existente. Você deve reproduzir um erro similar ao da mensagem abaixo:

AGENDA: DB error (Insert): -37 File: AGENDA – Error : 2601 (23000) (RC=-1) – [Microsoft][SQL Server Native Client 11.0][SQL Server]Cannot insert duplicate key row in object ‘dbo.AGENDA’ with unique index ‘AGENDA_UNQ’. The duplicate key value is (000006, 0).

No caso, cada Banco de Dados mostra uma mensagem similar com o mesmo significado — não foi possível realizar a inclusão pois os campos informados formam uma chave que já existe na tabela.

Observações sobre o índice único

Quando criamos um índice único para uma tabela no AdvPL, não é possível abrir este índice no AdvPL, usando por exemplo a função DbSetIndex(). O Objetivo do índice único é existir no Banco de Dados, para implementar uma camada de consistência das informações armazenadas diretamente no Banco de Dados. Porém, mesmo que não seja possível a sua abertura, é possível testar a sua existência usando a função TCCanOpen(), informando  o nome da tabela, e o nome do índice, como vemos no exemplo de código anterior.

Como fazer uma busca indexada – Função DBSeek()

Quando usamos um Banco de Dados relacional, normalmente criamos um índice para uma determinada tabela, e colocamos neste índice um ou mais campos — que vão fazer parte da chave do índice — com o objetivo do motor SQL ter um plano de ação otimizado, fazendo a seleção dos dados (SELECT) sobre o índice, baseado nas condições estabelecidas para trazer uma parte dos dados da tabela — realizada através de expressões condicionais na cláusula WHERE da QUERY.

Quando usamos a engine ISAM, e criamos um índice, a forma de fazermos uma busca optimizada é usar uma função de busca que utilize explicitamente o índice — função DBSeek() — que foi criado na aplicação, fornecendo no primeiro parâmetro um valor que seja composto da concatenação dos campos chave do índice — expressão de indexação — e o resultado será o posicionamento no primeiro registro que a chave de busca informada corresponde aos valores dos campos no registro.

No programa de exemplo, criamos 2 índices. O Índice chamado “AGENDA1”, cuja expressão de índice é apenas o campo “ID”, e o índice “AGENDA2”, com o campo “NOME”.  Estes índices são abertos logo após a abertura da tabela, e de acordo com a ordem de abertura, são selecionados como ordens de navegação ativa do ALIAS da tabela, usando a instrução DBSetorder(). No programa, alimentamos o campo ID na inclusão do registro, com um valor do tipo “CARACTERE”, contendo um valor numérico sequencial, de 6 posições, preenchido com zeros à esquerda: “000001”, “000002”,…

Pra realizar uma busca direta sobre o índice, pelo ID 000134, selecionamos o ALIAS da tabela — DbSelectArea() — depois nos certificamos que a ordem de navegação atual corresponde ao índice do campo chave “ID” — DBSetOrder() — e finalmente montamos uma chave de busca com o valor “000134” como “C” Caractere, e usamos a função DBSeek().

cChave := '000134'
DBSelectArea("AGENDA")
DbSetOrder(1)
IF DBSeek(cChave)
  MsgInfo('Chave ['+cChave+'] encontrada no registro '+cValToChar(Recno()))
Else
  MsgStop('Chave ['+cChave+'] NÃO ENCONTRADA')
Endif

Quando usamos esta instrução em uma tabela AdvPL aberta em modo de compatibilidade ISAM pelo DBAccess, devemos lembrar que no Banco de Dados a tabela é SQL, logo o DBAccess vai desmontar a sequencia de caracteres informada na função DBSeek() , e montar uma ou mais queries necessárias para emular o comportamento do ISAM e retornar se o registro realmente existe.

Baseado no trecho de código acima, podemos implementar algumas funcionalidades no código que perguntem ao operador um código ou um nome a ser pesquisado, e a função de pesquisa fará a busca pelo índice.

Diferença entre índices ISAM x SQL

Os índices criados para navegação e busca de dados usando um driver ISAM puro, como DBF / ADS, são criados em um ou mais arquivos físicos de indexação — para quem já programou em Clipper, quando usávamos a RDD DBFNTX, os arquivos de índices tinham a extensão “.NTX”, e cada arquivo suportava apenas um índice. Ao utilizarmos a RDD DBFCDX, os arquivos de índice tinham a extensão “.CDX”, e cada arquivo poderia suportar mais de uma chave de indexação. Uma particularidade interessante e ao mesmo tempo “perigosa” era a seguinte: Caso você criasse um índice para uma tabela, e ao fazer uma atualização na tabela — inclusão ou alteração — e esquecesse de abrir o arquivo de índices, o índice não era atualizado, o que causava comportamentos indesejáveis, como por exemplo uma busca pelo índice de uma informação que estava gravada na tabela não era encontrada.

Quando o DBAccess foi implementado para acesso a dados nos bancos relacionais — na época com o nome de TOPConnect — foi criado um mecanismo de emulação ISAM, de modo que cada índice que existia na aplicação original em DBF passou a existir também no Banco de Dados Relacional, justamente para que as operações de DBSeek() — busca sobre o índice — fosse tão performática quanto o ISAM, afinal o Banco de Dados já tinha um índice adequado para resolver as expressões de busca montadas em SQL quando usamos a função DBSeek(). A abertura de indices foi mantida igual, de modo que cada índice aberto no alias da tabela pode ser endereçado para mudar a ordem de navegação de acordo com a sequência de abertura. Para trocar a ordem para o primeiro índice aberto, usamos DBSetOrder(1), para o seguindo, DBSetOrder(2), e assim por diante. Caso seja usado um DBSetOrder(0), você passa a acessar a tabela pela ordem física de inclusão dos registros.

Por essas e outras razões de compatibilidade, as funções do Framework do ERP Microsiga abrem sempre a tabela e todos os índices existentes no dicionário ativo de dados (SXS) do ERP.

Conclusão

O assunto da busca indexada ISAM ainda têm mais pontos interessantes a serem vistos, vamos abordá-los no próximo post. Fique a vontade para baixar o código do programa Agenda.PRW — disponível do GitHub (https://github.com/siga0984/Blog) , e alterá-lo a seu critério. Confirme eu for implementando novas funcionalidades no programa, a versão do GIT será atualizada.

Novamente desejo a todos 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