quarta-feira, 14 de abril de 2010

Consultas "distinct" com XSL, otimizadas (o modo correto e o não tão correto...)

Esta dica foi enviada pelo meu amigo Luís Fernando Heckler.

As vezes é preciso percorrer os nodos do XML de forma semelhante a cláusula “distinct” do SQL. Por exemplo para gerar uma saída que tenha exatamente uma linha para cada tipo de nodo que estamos buscando no XML original, mas onde esses tipos podem aparecer repetidos.

Essa necessidade surgiu no projeto de um processo BPEL que alimenta um dashboard BAM.

A saída necessária era uma linha para cada departamento, mas no XML de origem os departamentos podiam aparecer mais de uma vez. Para iterar de forma única com XSLT 2.0, podemos utilizar a função xsl:for-each-group

Exemplo: [1]
<xsl:for-each-group select="//tns:departamento" group-by="./tns:dados/tns:nome">

Em [1] temos a expressão XPath que seleciona os nodos que nos interessam e a cláusula que especifica qual deve ser o índice, ou a chave única buscada, no caso, o nome do departamento. Dentro do laço, para identificarmos qual o departamento atual que está sendo processado, podemos usar a função current-grouping-key()

Exemplo: [2]
<xsl:variable name="departamentoAtual" select="current-grouping-key()"/>

Agora entra a dica principal: como fazer subconsultas sobre os nodos filhos de cada departamento, sendo que pode haver mais de uma ocorrência do mesmo departamento?

Primeiro o modo que funciona mas NÃO é otimizado

Digamos que você precise agora buscar todos os nodos “marca” que existem abaixo do departamento atual, que também devem ser buscados de forma única (um registro por marca). O natural é fazer outro for-each-group restringindo o nome do departamento no XPath de seleção das marcas, como abaixo:

Exemplo: [3]
<xsl:for-each-group select="//tns:marca[ancestor::*[local-name(.)='departamento']/tns:dados/tns:nome = $departamentoAtual]" group-by="./tns:dados/tns:nome">

Veja que em [3] estamos filtrando o nome do departamento usando a variável criada em [2], na seleção das marcas. Isso funciona perfeitamente, não está errado. Porém, exige que todo o XML a partir do nível dos departamentos seja parseado novamente, para poder fazer o filtro indicado.

Como melhorar? Detalhes a seguir…

Agora o modo otimizado (1/3 do tempo de processamento nos testes durante o desenvolvimento)

Aproveite as funções e índices em memória que o XSLT lhe proporciona, e não faça parser sobre a mesma parte gigante do arquivo 2x. A função current-group() é um índice em memória que contém o nodeset atual da iteração.

Exemplo: [4]
<xsl:for-each-group select="current-group()//tns:marca" group-by="./tns:dados/tns:nome">

Veja em [4] que estamos solicitando ao processador XSLT que itere as marcas não mais sobre o XML original, mas sobre o nodeset em memória da iteração atual dos departamentos, o que certamente é bem menor pois contém apenas os departamentos do mesmo nome atual e seus filhos.

E para “pegar” a marca atual? Use a função current-grouping-key() que agora está no contexto do laço das marcas, enquanto que o departamento atual nós já salvamos em uma variável lá no exemplo [2].

No desenvolvimento do processo BPEL que alimenta o BAM, uma transformação dessas que levava 3min passou a levar menos de 1min. Parece pouco? Mas são 4 delas por XML e 3 XML processados cada vez que a integração é executada... ;-)

Até a próxima, valeu Luís!

Nenhum comentário:

Postar um comentário