DB2 - ROWSET - Fetches e Inserts múltiplos


Volta a página anterior

Volta ao Menu Principal


Desenvolvido por DORNELLES Carlos Alberto - Analista de Sistemas - Brasília DF. - cad_cobol@hotmail.com

DB2 - ROWSET - Fetches e Inserts múltiplos

Publicação dirigida a profissionais que fazem arte em mainframe Nº 010 - Maio de 2.008

Pare de fazer tantas chamadas ao DB2! - Fetches e Inserts múltiplos!

O título desse boletim foi inspirado em um artigo publicado na revista z/Journal, entitulado 'Quit Calling DB2 So Much!', escrito por Susan Lawson e Daniel L. Luksetich, que fala sobre a formidável habilidade do DB2, a partir da versão 8, de efetuar FETCH e INSERT de conjuntos de registros, adicionalmente à manipulação de registros individuais.

Com o término do suporte ao DB2 versão 7 programado para 30 de junho de 2008, acredito que todos os nossos leitores já tenham à disposição o DB2 versão 8, para o qual as dicas a seguir são válidas.

Verifique, porém, se sua instalação do DB2 já está operando em New Function Mode, modo no qual são disponibilizadas aos desenvolvedores novas funcionalidades, como as que vamos ilustrar aqui.

  • Por que faço tantas chamadas ao DB/2?
    • Acho que nove em cada dez coboleiros nunca atentaram para o que vou falar aqui, e vão se surpreender: não há nada sobre SQL na padronização COBOL, e tudo o que colocamos entre o EXEC SQL e END-EXEC não pode ser interpretado diretamente pelo compilador COBOL.
    • Essa tarefa é realizada pelo pré-compilador DB2, que organiza os statements DB2 em um membro da DBRMLIB (biblioteca onde são guardados, separadamente, todos os comandos DB2 do programa) e troca todos os EXEC SQL por coisas que o compilador COBOL entende, como variáveis reservadas do DB2 e comandos CALL.
    • Se você tiver a curiosidade de capturar uma compilação e olhar o que é gerado pelo pré-compilador, verá que o SELECT, FETCH, INSERT e outros comandos do gênero são comentados e, em seu lugar, surge um CALL para a subrotina DSNHLI, que efetivamente realiza as operações no DB2.
    • Ou seja, todo ponto onde ocorre uma interface com o DB2 resulta em um CALL.
    • Não obstante às implicações do CALL - que ao ser usado em excesso prejudica a performance do programa, graças à troca constante de address space - há de se pensar no fato que cada chamada manipula apenas um registro.
    • Se o programa abrir um cursor para ler um milhão de registros, teremos ao menos um milhão de CALLs, descontando outros UPDATEs, COMMITs e afins.
    • Some-se a isso o fato de se aumentar o trabalho do DB2 em gerenciar seus bufferpools, que terão de manter os dados a serem lidos nas próximas chamadas em buffer para acelerar sua disponibilização para a aplicação (prefetch).
    • Muitos desenvolvedores adorariam que o DB2 fosse capaz de retornar lotes de registros, em vez de apenas um registro por vez - com arquivos seqüenciais, fazemos isso há décadas, graças ao advento da blocagem.
    • Porém, para atender a essa reivindicação, seria necessário fazer uma reengenharia no núcleo do DB2, permitindo que ele suportasse tanto a devolução de conjuntos de registros às linguagens de alto nível (arrays de dados), como também sua manipulação consistente, em formato de conjunto, pelos demais comandos de manipulação (DELETE, UPDATE, INSERT), sem no entanto ameaçar a compatibilidade com os comandos tradicionais, que manipulam um registro por vez.
    • Essa reengenharia foi feita no DB2 versão 8, que nos possibilita trabalhar com rowsets, ou conjuntos de rows (linhas), usando linguagens de programação de alto nível.
  • Seja muito bem-vindo, rowset!
    • Traduzindo ao pé da letra, rowset significa conjunto de registros. Um rowset é implementado pelas linguagens de alto nível como sendo um conjunto de tabelas individuais, uma para cada campo recuperado pelo DB2.
    • A título de exemplo, imagine uma cláusula SELECT que recupere CPF, nome, sexo e data de nascimento dos clientes, e que doravante desejamos manipulá-la usando um rowset, capaz de receber até 100 registros por vez.
    • A declaração da área de memória ficaria assim:

    • Veja que, dentro do nível 01, há quatro tabelas individuais, onde são armazenadas, em cada uma, até 100 ocorrências dos campos declarados no SELECT.
    • Essa abordagem é diferente da forma tradicional de organização de dados no COBOL, onde o OCCURS iria no nível 01, criando um conjunto de 100 registros contíguos, e não uma coleção de pequenas tabelas.
    • Portanto, cuidado para não se confundir. E nada de usar DEPENDING ON: as tabelas têm que ter tamanho fixo e igual à quantidade máxima de registros manipulados a cada chamada ao DB2.
  • Quero ler múltiplos registros: FETCH ROWSET
    • Alterar ou criar um programa para manipular blocos de registros é uma tarefa simples.
    • A primeira providência a tomar é criar, na sua STORAGE SECTION, um conjunto de tabelas onde serão armazenadas as múltiplas ocorrências retornadas pelo banco de dados, da forma como descrevi acima.
    • Em seguida, é preciso dizer ao DB2 que o seu cursor passará a retornar registros em forma de rowset, e isso se faz com a palavra reservada WITH ROWSET POSITIONING, assim:

    • A abertura e o fechamento do cursor não mudam em nada, mas muda a maneira de fazer o FETCH, já que agora o cursor devolve um bloco de registros.
    • Para compreender melhor como essa técnica funciona, veja a ilustração a seguir, onde temos um cursor cujo resultado aponta para um total de 100 registros, retornados ao programa em forma de rowset, com capacidade para 10 ocorrências.
    • Ao realizar o primeiro FETCH, são retornados os dez primeiros registros (de 01 a 10).
    • Se realizarmos um novo FETCH, mais dez registros serão retornados (de 11 a 20), e assim por diante.
    • Em outras palavras, se antes era preciso fazer 100 FETCHes para ler todos os dados, agora será preciso fazer apenas dez.
    • Se o rowset for ainda maior - 50 ocorrências, por exemplo - teremos que ir ao banco de dados apenas duas vezes, e isso é muito bom!

  • Por padrão, um cursor é lido sempre para a frente, sem saltos, usando FETCH NEXT ROWSET.
  • Porém, se o cursor for definido como SCROLL (rolável), o DB2 cria um ponteiro para o início do rowset, permitindo emitir FETCHes com indicação de posição: por exemplo, se o cursor tivesse sido definido como DECLARE CSR-CLI SENSITIVE DYNAMIC SCROLL CURSOR WITH ROWSET POSITIONING, seria possível, também, realizar operações de leitura para a frente e para trás, a partir de posições absolutas e relativas, usando diversas variações de FETCH, a saber:
    • FIRST - retorna o primeiro rowset do conjunto;
    • LAST - retorna o último rowset do conjunto;
    • NEXT - retorna o próximo rowset do conjunto;
    • PRIOR - retorna o rowset anterior do conjunto;
    • STARTING AT RELATIVE - retorna o próximo rowset iniciando a partir da posição relativa indicada;
    • STARTING AT ABSOLUTE - retorna o próximo rowset iniciando a partir da posição absoluta indicada.
  • Ok, e como é que nós codificamos o FETCH ROWSET em bom COBOL?
    • Continuando o exemplo do cursor hipotético da tabela TAB_CLIENTES, a chamada ao FETCH ficaria assim codificada:

    • Ficou fácil ver o que mudou:
    • O FETCH agora traz a palavra reservada ROWSET, e também indica ao DB2 a capacidade, em quantidade de registros, da área de dados fornecida pelo aplicativo para recebê-los.
    • Ou seja, estamos pedindo até 100 registros por FETCH. Quando digo 'até' 100 registros, isso se deve ao fato de nem sempre haver 100 registros disponíveis para devolução ao programa, e precisamos saber quantos registros foram, efetivamente, retornados pelo banco de dados, e isso é feito através da variável SQLERRD(3) da SQLCA.
    • Aí está uma boa mudança na forma padrão como tratamos cursores nas aplicações:
    • geralmente, lemos os cursores até o momento em que o SQLCODE retorna como +100 e, quando o código surge, significa que o último FETCH não leu dado algum - o ponteiro ultrapassou o último registro.
    • Quando lidamos com rowsets, há uma pequena e importante mudança conceitual:
    • Os FETCHes também deverão ser realizados até que o DB2 emita SQLCODE +100; porém, o último FETCH geralmente retorna, sim, registros válidos - só não retorna completamente 'cheio'.
    • Se um cursor aponta para 46 registros, o quinto FETCH NEXT ROWSET FOR 10 ROWS retornará SQLCODE +100, SQLERRD(3) igual a 6, e seis registros válidos (41 a 46).
    • O cursor descrito no exemplo não possui indicadores de nulidade, assumindo que todos os campos da tabela TAB_CLIENTES foram declarados como NOT NULL.
    • Porém, gostaria de encorajá-los a fazer definição de indicadores para todos os campos ao utilizar rowsets com cursores roláveis e sensitivos estáticos, em virtude da possibilidade de ocorrerem 'buracos' no processo de leitura: por exemplo, é possível que a aplicação delete um ou mais registros presentes no rowset, tornando-os inválidos.
    • Quando isso ocorre, a leitura retorna SQLCODE igual a +222, e as ocorrências onde os buracos ocorreram voltam com indicador igual a -3. Se o indicador não estiver declarado, teremos como resultado um erro.
  • Quero gravar múltiplos registros: INSERT ROWSET
    • Inserir múltiplos registros no banco de dados também é uma tarefa bastante simples, e se parece muito com a técnica usada para o FETCH múltiplo: preenchemos os campos das tabelas individuais do rowset com os dados que desejamos inserir, e depois emitimos o INSERT FOR nn ROWS, indicando quantas linhas desejamos inserir na tabela. E pronto!
    • Mas o que acontece se uma ou mais linhas do rowset não puderem ser inseridas na tabela - caso haja violação de chave primária, violação de chave estrangeira, indisponibilidade momentânea?
    • Depende:
    • a inserção pode ser realizada pelo DB2 de forma atômica (tudo ou nada) ou não atômica.
    • Se o INSERT for definido como ATOMIC, se qualquer um dos registros do rowset não puder ser inserido, todo o INSERT falha - o banco de dados faz rollback automático de todas as linhas inseridas com sucesso por aquele INSERT até o momento em que o erro ocorreu.
    • Se o INSERT for do tipo NOT ATOMIC CONTINUE ON SQLEXCEPTION, o processo de inserção continua, mesmo se um ou mais registros não puderem ser inseridos - daí, é responsabilidade do programador saber quais foram os registros não inseridos com sucesso, com o uso do comando GET DIAGNOSTICS, sobre o qual falaremos em maior detalhe daqui a pouco.
    • Há vários indicadores, alimentados pelo DB2, que servem para saber o que aconteceu durante o processo de INSERT múltiplo, os seguintes:

    • Caso você decida por usar o modo NOT ATOMIC, seu programa precisa implementar controles adicionais, utilizando o comando GET DIAGNOSTICS.
    • Esse controle é geralmente realizado em dois passos: quando o SQLCODE do INSERT retorna diferente de zero - sinalizando a ocorrência de algum erro ou warning, é preciso, primeiramente, saber quantos erros ocorreram, e isso se faz atribuindo a quantidade de erros (que corresponde à variável NUMBER do GET DIAGNOSTICS) a uma variável host.
    • Depois de obtida a quantidade de erros, emitimos tantos GET DIAGNOSTICS CONDITION quantos forem os erros, para obter detalhes sobre cada um deles, em um loop do tipo PERFORM UNTIL.
    • O trecho de código abaixo ilustra como fazer um controle de erros usando INSERTs não atômicos:
    • por exemplo, imagine que as linhas não inseridas, por qualquer motivo, devam ser escritas em um arquivo de descartes (ddname SYSDISC), a ser analisado posteriormente. O código poderia ser escrito assim:

    • Neste trecho de exemplo, o primeiro GET DIAGNOSTICS recupera a quantidade de erros detectados e o coloca em uma variável host do COBOL, declarada em storage section como sendo do tipo S9(04) COMP ou seja, smallint.
    • Depois, elaborei um contador que percorre a quantidade de erros, recuperando informações de cada condição de erro detectada pelo DB2 com uso de GET DIAGNOSTICS CONDITION :I, onde o "I" é um contador, incrementado de um a cada chamada, e indica a ocorrência de erro sobre a qual queremos saber mais.
    • O DISPLAY é opcional, e foi colocado apenas para fins didáticos, assim podemos ver em SYSOUT o resultado da chamada.
    • Logo abaixo, há algumas movimentações, pegando os dados da tabela em memória (que armazena o rowset), na posição onde o erro de INSERT ocorreu, e colocando esses dados na área de dados do arquivo SYSDISC (os campos iniciados por "FD" estão descritos na FILE SECTION como partes do arquivo apontado por SYSDISC), que é gravado pelo comando WRITE logo a seguir.
    • Como você certamente notou, a inserção não atômica dá um bom trabalho extra.
    • Porém, a possibilidade de continuar o processo de inserção em lotes sem interromper o processamento, mesmo quando um ou mais registros têm problemas, é interessante em aplicações de missão crítica, que não podem ser interrompidas para análise imediata do registro inconsistente.
  • Quero um programa esqueleto, para poder copiar!
    • A máxima "na informática, nada se cria, tudo se copia" nunca foi tão verdadeira!
    • Para acelerar a adoção das novas técnicas de INSERT e FETCH múltiplos na sua empresa, fiz um programa de exemplo escrito em COBOL, que você pode baixar do site, transferir para seu mainframe, compilar e executar.
    • Ele pode ser livremente alterado e reutilizado, sem qualquer custo - ele é livre!
    • Para que o programa funcione corretamente, é preciso que você tenha à disposição as 'sample tables' do DB2, que são criadas durante a instalação do produto.
    • Ele faz uso da tabela EMP (owner DSN8810), que antes deve ser copiada para o seu database pessoal (de teste) com os seguintes comandos:

    • Por que realizar a cópia?
    • Simples: a tabela EMP é de uso comum por todos os demais usuários do DB2, e nosso programa de teste fará INSERTs na tabela.
    • Se o fizermos diretamente na tabela DSN8810.EMP, estaremos 'sujando' a sample table original do DB2, e se mais usuários da sua empresa decidirem rodar este programa de teste no mesmo ambiente, teremos problemas com registros duplicados.
    • Então, lembre-se sempre de copiar a tabela original e, ao compilar o programa, substitua o owner DSNUSER pelo seu owner pessoal, geralmente igual à sua chave de acesso ao DB2.
  • Se você usa a técnica tradicional de FETCH único há muito tempo, é possível que você precise de uma injeção extra de ânimo para mudar a forma como você escreve seus programas, afinal, se funciona, pra quê mudar?
  • O incentivo advém da performance: a IBM fez testes que indicam ganhos, tanto de CPU como de "tempo de sala", realmente assombrosos.
  • Se você, como eu, gosta de programas rápidos e enxutos, essa técnica permite obter resultados excelentes, com pouquíssimo esforço adicional de desenvolvimento.
  • Faça seus testes, e me envie os resultados. Mês que vêm tem mais, até lá!
  • Tulio Lazarini (30) trabalha na Diretoria de Tecnologia desde 2002, e desenvolve aplicativos em ambiente mainframe na Gerência de Projetos Corporativos.
  • Dúvidas, críticas e sugestões de pauta podem ser encaminhadas para o endereço de correio eletrônico tulio.lazarini@gmail.com
  • Participe da Comunidade Mainframe Brasil no developerWorks!
  • A comunidade Mainframe Brasil é o espaço brasileiro no developerWorks para profissionais que desenvolvem ou suportam aplicações em ambiente mainframe.
  • Neste espaço, são divulgados eventos, publicações, manuais, cursos e informações relevantes para quem é do meio.
  • É o ponto de encontro dos iniciantes na plataforma com profissionais mais experientes.
  • A comunidade também oferece um fórum de discussão, o Fórum Mainframe Brasil, de participação absolutamente LIVRE e GRATUITA, onde falamos sobre tudo que diga respeito a mainframe.
  • Gostaria de convidá-lo a participar conosco deste espaço em bom português, compartilhando e construindo conhecimento.
  • Amplie seu networking, esteja em contato com profissionais da nossa área.