Retornar sequências xml em que um atributo não contém um caractere específico

10

Considere o seguinte XML simples:

<xml>
  <customer name="Max">
    <email address="[email protected]" />
  </customer>
  <customer name="Erik">
    <email address="[email protected]" />
  </customer>
  <customer name="Brent">
    <email address="brentcom" />
  </customer>
</xml>

Quero obter uma lista de <Customer>sequências em que o addressatributo do <email>item não contém um @.

Então, eu quero uma saída que se parece com:

<customer name="Brent">
  <email address="brentcom" />
</customer>

mcve :

DECLARE @x XML = '<xml>
<customer name="Max"><email address="[email protected]" /></customer>
<customer name="Erik"><email address="[email protected]" /></customer>
<customer name="Brent"><email address="brentcom" /></customer>
</xml>';

Esta consulta:

SELECT WithValidEmail = @x.query('/xml/customer/email[contains(@address, "@")]')
    , WithInvalidEmail = @x.query('/xml/customer/email[contains(@address, "@")] = False');

Devoluções:

╔═══════════════════════════════════════╦══════════════════╗
            WithValidEmail              WithInvalidEmail 
╠═══════════════════════════════════════╬══════════════════╣
 <email address="[email protected]" />                          
 <email address="[email protected]" />  false            
╚═══════════════════════════════════════╩══════════════════╝

Esta consulta:

SELECT WithInValidEmail = @x.query('/xml/customer/email')
WHERE @x.exist('/xml/customer/email[contains(@address, "@")]') = 0;

Devoluções:

╔══════════════════╗
 WithInValidEmail 
╚══════════════════╝
    (no results)

A WHEREcláusula na consulta acima está eliminando todo o conjunto de XML porque existe pelo menos uma única sequência em que o endereço de email contém um sinal "@".

Max Vernon
fonte

Respostas:

11

Uma maneira fácil de fazer isso é usar o nodes método para ir direto ao addressatributo e verificar seu @sinal.

O problema com o jeito que você está olhando agora é que ele está apenas checando se algum endereço de e-mail possui um @. A análise dos nós XML permite verificar e-mails individuais.

DECLARE @x XML
    = '<xml>
<customer name="Max"><email address="[email protected]" /></customer>
<customer name="Erik"><email address="[email protected]" /></customer>
<customer name="Brent"><email address="brentcom" /></customer>
</xml>';


SELECT x.c.value('@address', 'VARCHAR(100)') AS [email]
FROM   @x.nodes('/xml/customer/email') AS x(c)
WHERE  x.c.exist('@address[contains(., "@")]') = 0;

Se você precisar consultar uma tabela real com uma coluna XML como esta, basta CROSS APPLYusar o método de nós da seguinte maneira:

SELECT x.c.value('@address', 'VARCHAR(100)') AS [email]
FROM @x_table AS xt
CROSS APPLY xt.x.nodes('/xml/customer/email') AS x(c)
WHERE  x.c.exist('@address[contains(., "@")]') = 0;

Se você quiser trazer todo o <customer>...</customer>XML para essa "linha" de volta, poderá retornar o eixo. Lembre-se de que voltar pode tornar o desempenho um pouco problemático para grandes blocos XML.

SELECT x.c.query('..')
FROM @x_table AS xt
CROSS APPLY xt.x.nodes('/xml/customer/email') AS x(c)
WHERE  x.c.exist('@address[contains(., "@")]') = 0;

Outra maneira de fazer isso é:

SELECT @x.query('/xml/customer[email/@address[not(contains(., "@"))]]') answer

Mover os colchetes para envolver o nó do email efetivamente faz com que a WHEREcláusula seja aplicada ao customernó. A tradução deste XQuery para o inglês se parece com:

Obter todos os xml/customernós com um emailnó que possui um addressatributo que não contém o @símbolo

Erik Darling
fonte
4

Você estava tão perto. Você estava definitivamente no caminho certo ao usar a .query()função e a containsfunção XQuery. O que você errou foi:

  1. Colocar o = False exterior do [...](significando que não fazia parte da contains()expressão)
  2. Usando a palavra em Falsevez da funçãofalse()
  3. Não especificando o nó pai adicionando /..ao final do caminho (para que o resultado inclua o <customer>elemento e não apenas o <email>elemento)

A correção dessas três coisas resulta na seguinte expressão XQuery que dá o que você deseja:

'/xml/customer/email[contains(@address, "@") = false()]/..'

Colocar isso no seu exemplo original da pergunta fornece:

DECLARE @x XML = '<xml>
<customer name="Max"><email address="[email protected]" /></customer>
<customer name="Erik"><email address="[email protected]" /></customer>
<customer name="Brent"><email address="brentcom" /></customer>
</xml>';

SELECT
@x.query('/xml/customer/email[contains(@address, "@")]/..') AS [WithValidEmail],
@x.query('/xml/customer/email[contains(@address, "@")=false()]/..') AS [WithInvalidEmail;

Essa consulta retorna o seguinte conjunto de resultados de uma única linha com dois campos XML:

WithValidEmail                            |     WithInvalidEmail
<customer name="Max">                     |     <customer name="Brent">
  <email address="[email protected]" />          |       <email address="brentcom" />
</customer>                               |     </customer>
<customer name="Erik">                    |
  <email address="[email protected]" />   |
</customer>                               |

Provavelmente, isso é mais eficiente do que interromper o documento com a .nodes()função, pois ele pode analisar o XML em uma única captura e não é necessário iniciar e parar o analisador por cada nó.

O outro benefício de mantê-lo dentro .query()é que você recebe um único documento XML retornado. Portanto, se você receber um documento / valor XML que contém vários nós, vale a pena manter a abordagem de valor escalar de ser uma entidade única sem precisar reconstruir os nós resultantes novamente em um documento novamente. Isso também permite usá-lo em uma subconsulta / CTE sem alterar o número de linhas esperadas que estão sendo retornadas.

Solomon Rutzky
fonte