XQuery / BaseX - Limite de profundidade do resultado

Oct 04 2020

Ao usar XPath ou XQuery, há uma maneira de limitar a profundidade do resultado?

Estou usando o BaseX, que oferece suporte a XQuery 3.1 e XSLT 2.0.

Por exemplo, dado este documento de entrada:

<country name="United States">
  <state name="California">
    <county name="Alameda" >
      <city name="Alameda" />
      <city name="Oakland" />
      <city name="Piedmont" />
    </county>
    <county name="Los Angeles">
      <city name="Los Angeles" />
      <city name="Malibu" />
      <city name="Burbank" />
    </county>
    <county name="Marin">
      <city name="Fairfax" />
      <city name="Larkspur" />
      <city name="Ross" />
    </county>
    <county name="Sacramento">
      <city name="Folsom" />
      <city name="Elk Grove" />
      <city name="Sacramento" />
    </county>
  </state>
</country>

Se eu executar esta consulta:, /country/stateobtenho o seguinte resultado:

<state name="California">
  <county name="Alameda">
    <city name="Alameda"/>
    <city name="Oakland"/>
    <city name="Piedmont"/>
  </county>
  <county name="Los Angeles">
    <city name="Los Angeles"/>
    <city name="Malibu"/>
    <city name="Burbank"/>
  </county>
  <county name="Marin">
    <city name="Fairfax"/>
    <city name="Larkspur"/>
    <city name="Ross"/>
  </county>
  <county name="Sacramento">
    <city name="Folsom"/>
    <city name="Elk Grove"/>
    <city name="Sacramento"/>
  </county>
</state>

Eu gostaria de limitar a profundidade do resultado. Idealmente, haveria uma maneira de especificar a profundidade, em vez de codificar uma consulta XPath.

Como exemplo, gostaria de limitar o resultado aos nós de resultado e seus filhos, mas não incluindo os netos, então o resultado seria:

<state name="California">
  <county name="Alameda" />
  <county name="Los Angeles" />
  <county name="Marin" />
  <county name="Sacramento" />
</state>

Respostas

5 zx485 Oct 04 2020 at 05:58

Uma maneira fácil e direta é usar o XSLT-2.0 com um modelo vazio cancelando todos os filhos de <county>. O <xsl:strip-space>remove o espaço que seria usado pelas crianças.

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
  <xsl:strip-space elements="*" />
 
  <!-- Identity template -->
  <xsl:template match="@* | node()">
    <xsl:copy>
      <xsl:apply-templates select="@* | node()" />
    </xsl:copy>
  </xsl:template>
  
  <xsl:template match="/">
      <xsl:apply-templates select="/country/state" />
  </xsl:template>
  
  <xsl:template match="county/*" />
  
</xsl:stylesheet>

O resultado é:

<?xml version="1.0" encoding="UTF-8"?>
<state name="California">
    <county name="Alameda"/>
    <county name="Los Angeles"/>
    <county name="Marin"/>
    <county name="Sacramento"/>
</state>

Com XQuery, uma solução poderia ser assim:

for $st in doc("b.xml")/country/state return element { node-name($st) } { $st/@*, for $ct in $st/county return element { node-name($ct) } { $ct/@* }
  }

A saída é a mesma.

1 MichaelKay Oct 04 2020 at 18:14

Na verdade, o resultado da sua consulta é um único nó, o statenó no documento de origem. Alguns softwares estão exibindo os resultados da consulta - ou seja, o statenó - em algum formato específico, mas, em princípio, os resultados podem ser exibidos de uma maneira diferente sem alterar a consulta. Por exemplo, estou ciente de um software que exibiria os resultados desta consulta como

/country[1]/state[1]

Portanto, você precisa separar duas questões: quais nós a consulta retorna e como eles são exibidos? Em alguns casos, pode fazer sentido criar um pipeline de processamento em que a primeira etapa seleciona os nós de interesse e a segunda etapa controla a apresentação dos resultados.

Pessoalmente, eu sempre faria a segunda etapa em XSLT, mas algumas pessoas preferem XQuery. Faça sua escolha.

1 MikeChristiansen Oct 06 2020 at 20:05

A postagem de @ zx845 me colocou no caminho certo. Meu objetivo final era limitar a profundidade do resultado, com a intenção de obter um "resumo" e os metadados de que preciso para obter resultados mais profundos, se necessário.

BaseX tem uma função "db: node-id" que retornará o ID do nó interno de qualquer nó dado. Existe outra função, "db: open-id" que retorna o nó com um determinado ID.

Suponha que esta entrada seja dada:

<country name="United States">
  <state name="California">
    <county name="Alameda">
      <city name="Alameda"/>
      <city name="Oakland"/>
      <city name="Piedmont"/>
    </county>
    <county name="Los Angeles">
      <city name="Los Angeles"/>
      <city name="Malibu"/>
      <city name="Burbank"/>
    </county>
    <county name="Marin">
      <city name="Fairfax"/>
      <city name="Larkspur"/>
      <city name="Ross"/>
    </county>
    <county name="Sacramento">
      <city name="Folsom"/>
      <city name="Elk Grove"/>
      <city name="Sacramento"/>
    </county>
  </state>
  <state name="New York">
    <county name="Albany">
      <city name="Albany"/>
      <city name="Cohoes"/>
      <city name="Watervliet"/>
    </county>
    <county name="Erie">
      <city name="Buffalo"/>
      <city name="Lackawanna"/>
      <city name="Tonawanda"/>
    </county>
  </state>
</country>

Eu defini essa função, que me permite controlar a profundidade e retornar o id do nó para cada nó.

declare function local:abbreviated($input, $depth as xs:integer)
{
  if($depth = 0) then element node { db:node-id($input)
    }
  else
    element { node-name($input) } { attribute node-id { db:node-id($input)
      },
      $input/@*, $input/text(),
      for $child in $input/*
        return local:abbreviated($child, $depth - 1)
    }
};

Se eu executar o seguinte:

declare variable $input := /country/state; for $result in $input return local:abbreviated($result, 1)

Então eu obtenho este resultado:

<state node-id="3" name="California">
  <node>5</node>
  <node>13</node>
  <node>21</node>
  <node>29</node>
</state>
<state node-id="37" name="New York">
  <node>39</node>
  <node>47</node>
</state>

Agora, quando eu processo os resultados, se o usuário deseja mais detalhes para um stateelemento, posso processar cada elemento do 'nó' e executar esta consulta para obter o conteúdo real do nó

local:abbreviated(db:open-id('states', 5), 2)

Resultando em:

<county node-id="5" name="Alameda">
  <city node-id="7" name="Alameda"/>
  <city node-id="9" name="Oakland"/>
  <city node-id="11" name="Piedmont"/>
</county>