Статьи

SQLX — от БД прямо к XML и обратно

Запрос к таблицам БД, получение XML вместо набора результатов

В большинстве современных корпоративных приложений есть постоянно меняющаяся область, заставляющая разработчиков каждый раз пересматривать модель данных: часть кода, в которой таблицы БД отображаются в структуру XML. То, как это делается, варьируется от одного случая к другому, но цель всегда одна — создать XML из реляционных данных, а более сложная часть — сделать это в обратном порядке, обновить базу данных на основе входящих данных в формате XML.


Возможные решения:

  • Вероятно, худшее решение, которое вы можете выбрать, это запросить базу данных, интерпретировать набор результатов и построить DOM в необработанном коде. а затем закодируйте все это снова и прочитайте DOM, интерпретируйте его и создайте оператор обновления. Почему? Вы только что сгенерировали огромное количество связующего кода, и отображение очень сложно поддерживать.

  • Лучший подход — создавать изолированные бизнес-объекты в соответствии с желаемой структурой XML, заполнять их данными из запроса и сериализовать / маршалировать их в XML с помощью JAXB. Теперь гораздо проще реализовать это задом наперед. Вы даже можете обогатить это решение с помощью JPA, но будьте осторожны, демонтаж сущностей JPA из XML не является хорошей идеей при обновлении!  

  • Круто и модно было бы использовать поддержку XQuery на вашем сервере БД. Как насчет запросов к данным БД и вместо набора результатов получить нужный XML-документ? Но есть но. XQuery молод, он стал Рекомендацией W3C 23 января 2007 года. Поддержка в машинах БД все еще развивается, и, на мой взгляд, наиболее важным «но» является то, что он все еще смешивает два языка (SQL и XQuery) для выполнения одного запроса. Это многообещающе, но …

  • Мой любимый SQL / XML или сокращенный SQLX, мы вернемся к нему сразу после короткого примера XQuery.

Будь крут с XQuery


«Но» реализация отличается в Oracle DB и IBM DB2.
Ниже приведен пример для DB2, в Oracle DB вы не можете (вы можете использовать sqlplus, но это совсем другая история) напрямую заменить оператор SQL на XQuery, вы должны использовать функции XMLQuery () и XMLTable () и фактически выбирать из результата XQuery. Такое встраивание XQuery в оператор SQL возможно в обоих случаях с небольшими отличиями.

оракул
SELECT XMLQUERY('$y/desc/title' 
       PASSING indoc AS "y" RETURNING CONTENT) 
INTO titlexml FROM HR.HR_DOCS;

DB2
SELECT XMLQUERY('$y/desc/title' 
       PASSING indoc AS "y") 
INTO titlexml FROM HR.HR_DOCS;

В DB2 можно встроить SQL-запрос в оператор XQuery, как в этом примере
xquery
<ErrorCatalog>{
for $y in 
db2-fn:sqlquery(
'SELECT XMLELEMENT(NAME "TmpErr",
        XMLATTRIBUTES(e.ERRCODE AS "code"),
        XMLTEXT(e.DESC)) FROM CC.CC_ERROR e')
where $y/@code=404
return (
   <Error> {$y/@code} {$y/text()}</Error>
)}</ErrorCatalog>

Выход
<ErrorCatalog>
  <Error code="404">
    The requested resource could not be found 
    but may be available again in the future. 
    Subsequent requests by the client are 
    permissible.
  </Error>
</ErrorCatalog>

Если вы зайдете достаточно глубоко, вы обнаружите, что обратное встраивание XQuery в SQL, с другой стороны, немного более удобно в БД Oracle.


Но при попытке использовать XQuery вы сталкиваетесь с XMLELEMENT (), XMLFOREST (), XMLAGG () и так далее.
И это SQL / XML, инструмент, с помощью которого сам SQL может создавать XML.

SQL / XML

Краткая история SQL / XML


Все началось в октябре 2000 года, когда отраслевой консорциум под названием SQLX Group начал работу по расширению поддержки SQL для запросов XML из базы данных.
Основными отраслями SQLX Group являются такие известные имена, как Hewlett-Packard, IBM, Microsoft, Oracle, Sybase и многие другие. Первая редакция SQL появилась 3 года спустя,
SQL: 2003-14 принес базовые возможности XML, как мы их знаем, а спустя годы XQuery начал присоединяться к игре. Функция XMLTABLE () появилась в версии SQL / XML: 2006.

Начнем с простого выбора

Мы будем использовать функцию XMLELEMENT и XMLAGG, которые объединяют все найденные строки под одним родительским элементом. XMLAGG — действительно мощный инструмент, потому что с
GROUP BY  можно сортировать сгруппированные строки как элементы по разным родителям. Но оставь это сейчас.

SELECT XMLELEMENT(NAME "PhoneBook", -- root element name
                  XMLAGG(-- aggregation over the rows	
                  XMLELEMENT(NAME "Contact",                                                  XMLATTRIBUTES(cust.FIRST_NAME AS "Name",
                                cust.TEL)
                              )
                        )
                  )
FROM TMP.CUSTOMER AS cust;

Выход

<PhoneBook>
    <Contact Name="Daniel" TEL="788255855"/>
    <Contact Name="Martin" TEL="889665447"/>
    <Contact Name="Eva"    TEL="111222333"/>
    <Contact Name="Alena"  TEL="444555666"/>
    <Contact Name="Oliver" TEL="777888999"/>
    <Contact Name="George" TEL="444882446"/>
    <Contact Name="Jamie"  TEL="123456789"/>
</PhoneBook>

Чтобы увидеть результат сразу же, мы должны сериализовать его:

XMLSERIALIZE (XMLELEM …) AS VARCHAR (500))

или же:

XMLSERIALIZE (XMLELEM …) КАК КЛОБ (500), ВКЛЮЧАЯ XMLDECLARATION)

Включение объявления xml добавляет <? Xml version … так что он стал полноценным XML-документом, но работает только в DB2. Еще одним преимуществом сериализации xml является то, что нам не нужен какой-либо специальный новый драйвер jdbc, который знает типы XML.

Пространства имен

Функция XMLNAMESPACES () доступна только в DB2 8.2 и выше, странным образом в Oracle вы можете использовать XMLNAMESPACES () в EXTRACT (), но не в XMLELEMENT (), в Oracle и PostgreSql использовать функцию XMLATTRIBUTES () и добавлять xmlns вручную.
Пространство имен по умолчанию в DB2

SELECT XMLSERIALIZE(CONTENT 
        XMLELEMENT(NAME "PhoneBook",
        XMLNAMESPACES(DEFAULT 'http://daniel.kecovi.cz/tmp/1.0'),
        XMLAGG(
        XMLELEMENT(NAME "Contact",
         XMLATTRIBUTES(cust.FIRST_NAME||' '||cust.LAST_NAME AS "Name",
                                             cust.TEL)
                              )
                  ))AS VARCHAR(500))
FROM TMP.CUSTOMER AS cust;

Пространство имен по умолчанию в Oracle

SELECT XMLSERIALIZE(CONTENT 
        XMLELEMENT(NAME "PhoneBook",
        XMLATTRIBUTES('http://daniel.kecovi.cz/tmp/1.0' AS "xmlns"),
         XMLAGG(
          XMLELEMENT(NAME "Contact",
           XMLATTRIBUTES(cust.FIRST_NAME||' '||cust.LAST_NAME AS "Name",
                                             cust.TEL)
                              )
                  ))AS VARCHAR(500))
FROM TMP.CUSTOMER AS cust;

Выход

<PhoneBook xmlns="http://daniel.kecovi.cz/tmp/1.0">
    <Contact Name="Daniel Kec"   TEL="788255855"/>
    <Contact Name="Martin Kec"   TEL="889665447"/>
    <Contact Name="Eva Kec"      TEL="111222333"/>
    <Contact Name="Alena Kec"    TEL="444555666"/>
    <Contact Name="Oliver Kec"   TEL="777888999"/>
    <Contact Name="George Takei" TEL="444882446"/>
    <Contact Name="Jamie Oliver" TEL="123456789"/>
</PhoneBook>

Декларация пространства имен различается в oracle и postgre, вам нужно вручную добавить атрибут следующим образом:   XMLATTRIBUTES (‘http://daniel.kecovi.cz/tmp/1.0’ AS «xmlns»)

Должен ли я повториться?

There is a thing with which you are going to meet when building some larger documents. One element which is on multiple different places in the document tree. You don’t have to write redundant code in your SQL query, because there is WITH. With clause allows you to
create common table expression before query itself, so why not use it as a function? Instead of pre preparing table we can just pre prepare table with reusable XML fragment.

Tested on DB2 9.7 and Oracle 11g only difference is dummy table name

WITH 
-- prepared to create Customer element, 
-- lastName column is possible filtering
tmpCustomer(lastName,custEl) AS(
SELECT LAST_NAME,XMLELEMENT(NAME "Customer",
                  XMLATTRIBUTES(FIRST_NAME||' '||LAST_NAME AS "FullName"
                                   )
                    )
FROM TMP.CUSTOMER cust
)
-- main select
SELECT XMLSERIALIZE(
        CONTENT
        XMLELEMENT(NAME "CustomerList",
         -- using prepared Customer
         (SELECT XMLAGG(t1.custEl) FROM tmpCustomer t1),
         XMLELEMENT(NAME "KecFamily",
         -- using prepared Customer 
         -- and filtering only those with last name kec
          (SELECT XMLAGG(t2.custEl) 
           FROM tmpCustomer t2 
           WHERE t2.lastName='Kec')
                    )                                     
                  )AS CLOB)
-- in this example using dummy table
-- (dual for oracle and SYSIBM.SYSDUMMY1 for db2)
--FROM DUAL;
FROM SYSIBM.SYSDUMMY1;

Output

<CustomerList>
    <Customer FullName="Daniel Kec" />
    <Customer FullName="Martin Kec" />
    <Customer FullName="Eva Kec" />
    <Customer FullName="Alena Kec" />
    <Customer FullName="Oliver Kec" />
    <Customer FullName="George Takei" />
    <Customer FullName="Jamie Oliver" />
    <KecFamily>
        <Customer FullName="Daniel Kec" />
        <Customer FullName="Martin Kec" />
        <Customer FullName="Eva Kec" />
        <Customer FullName="Alena Kec" />
        <Customer FullName="Oliver Kec" />
    </KecFamily>
</CustomerList>

Ah please I need variables

Little help with variables brings WITH.

On DB2 simple like this

WITH 
vars(
DATETIMEMASK,               -- datetime mask
DATEMASK,                   -- date mask
TIMEMASK                    -- only time mask
) 
AS (VALUES(                 
'DD.MM.YYYY HH24:MI',     
'DD.MM.YYYY',               
'HH24:MI'))       
-- main select
SELECT XMLSERIALIZE(
        CONTENT
        XMLELEMENT(NAME "CustomerList",
          XMLAGG(
          XMLELEMENT(NAME "Customer",
          XMLATTRIBUTES(c.FIRST_NAME AS "Name",
TO_CHAR(c.LAST_ORDER,vars.DATEMASK) AS "LastOrder"
                                 )
                             )                            
                          )
                  )AS CLOB)
FROM vars,TMP.CUSTOMER c;

Oracle need SELECT something 

WITH 
vars(
DATETIMEMASK,           -- datetime mask
DATEMASK,               -- date mask
TIMEMASK                -- only time mask
) 
AS (SELECT                
'DD.MM.YYYY HH24:MI',     
'DD.MM.YYYY',               
'HH24:MI' 
FROM DUAL)                 
-- main select
SELECT XMLSERIALIZE(
        CONTENT
        XMLELEMENT(NAME "CustomerList",
         XMLAGG(
         XMLELEMENT(NAME "Customer",
         XMLATTRIBUTES(c.FIRST_NAME AS "Name",
TO_CHAR(c.LAST_ORDER,vars.DATEMASK) AS "LastOrder"
                                 )
                             )                            
                          )
                  )AS CLOB)
FROM vars,TMP.CUSTOMER c;

Output

<CustomerList>
    <Customer Name="Daniel" LastOrder="22.05.2013" />
    <Customer Name="Martin" LastOrder="22.05.2013" />
    <Customer Name="Eva"    LastOrder="22.05.2013" />
    <Customer Name="Alena"  LastOrder="22.05.2013" />
    <Customer Name="Oliver" LastOrder="22.05.2013" />
    <Customer Name="George" LastOrder="22.05.2013" />
    <Customer Name="Jamie"  LastOrder="22.05.2013" />
</CustomerList>

Expression as element name

Maybe you wonder how to make an element with name other than constant. On Oracle you can use EVALNAME() function:

SELECT XMLSERIALIZE(CONTENT 
        XMLELEMENT(NAME "PhoneBook",   
         XMLAGG(
          XMLELEMENT(EVALNAME(cust.FIRST_NAME),
           XMLATTRIBUTES(cust.LAST_NAME AS "LastName",
                                             cust.TEL)
                              )
                  ))AS VARCHAR(500))
FROM KYTYR.CUSTOMER cust;

And back to the Table

We have been dealing only with querying tables until now but what about inserts and updates? It is no problem with XMLTABLE which enable us to map the XML document to temporary table with XPath. Then it’s old plain SQL again.

Example of updates which works on both Oracle 11g and DB2 9.7

INSERT INTO TMP.DISCOUNT_CODE 
(SELECT discount_code, rate 
 FROM XMLTABLE('$doc/discount' PASSING 
                XMLPARSE(DOCUMENT 
'<discount>
 <code>H</code>
 <rate>5</rate>
 </discount>') as "doc"
       COLUMNS
          discount_code CHAR    PATH 'code',
          rate          DECIMAL PATH 'rate' ));

UPDATE TMP.DISCOUNT_CODE
SET (discount_code, rate) = (
    SELECT discount_code, rate
    FROM XMLTABLE('$doc/discount' PASSING 
                  XMLPARSE(DOCUMENT 
'<discount>
 <code>H</code>
 <rate>6</rate>
 </discount>') as "doc"
       COLUMNS
          discount_code CHAR    PATH 'code',
          rate          DECIMAL PATH 'rate' )
) WHERE discount_code = 'H';

Recreating of the sample table TMP.CUSTOMER, tested on DB2 9.7 and ORACLE 11g

CREATE TABLE TMP.CUSTOMER
(
ID int NOT NULL PRIMARY KEY,
FIRST_NAME varchar(255),
LAST_NAME  varchar(255),
LAST_ORDER TIMESTAMP,
TEL        DECIMAL(20)
);


INSERT INTO TMP.CUSTOMER(ID, FIRST_NAME, LAST_NAME,LAST_ORDER, TEL)
(SELECT id,first_name,last_name,CURRENT_TIMESTAMP,tel 
 FROM XMLTABLE('$doc/PhoneBook/Contact' PASSING 
                         XMLPARSE(DOCUMENT 
'<PhoneBook>
    <Contact id="1"        
             Name="Daniel" 
             Surname="Kec" 
             TEL="788255855"/>
    <Contact id="2"        
             Name="Martin" 
             Surname="Kec" 
             TEL="889665447"/>
    <Contact id="3"        
             Name="Eva"    
             Surname="Kec"    
             TEL="111222333"/>
    <Contact id="4" 
             Name="Alena"  
             Surname="Kec"    
             TEL="444555666"/>
    <Contact id="5" Name="Oliver" 
             Surname="Kec"    
             TEL="777888999"/>
    <Contact id="6" 
             Name="George" 
             Surname="Takei"  
             TEL="444882446"/>
    <Contact id="7" 
             Name="Jamie"  
             Surname="Oliver" 
             TEL="123456789"/>
</PhoneBook>') as "doc"
       COLUMNS
          id         INT         PATH '@id',
          first_name VARCHAR(50) PATH '@Name',
          last_name  VARCHAR(50) PATH '@Surname',  
          tel        DECIMAL(20) PATH '@TEL' ));


TMP.CUSTOMER

ID FIRST_NAME LAST_NAME LAST_ORDER TEL
1 Daniel Kec 2013-05-21 10:28:26.614 788255855
2 Martin Kec 2013-05-21 10:30:09.294 889665447
3 Eva Kec 2013-05-16 10:30:10.422 111222333
4 Alena Kec 2013-05-21 10:30:13.550 444555666
5 Oliver Kec 2013-05-01 10:30:14.486 777888999
6 George Takei 2013-02-05 10:30:19.246 444882446
7 Jamie Oliver 2012-07-17 10:30:23.174 123456789

Comparison of XML functions support across DB machines

Comparison is not easy because lot of functions can be replaced by different approach or another function. So missing “+” in the table below means in the most cases that function has an alternative, for example XMLCOLATTVAL is almost synonym to XMLFOREST, XMLROOT is almost same thing as XMLDOCUMENT and so on.

Date of release 2004 2006 2009 2012 2011 2003 2005 2010
funtions DB2 8.2 DB2 9.1 DB2 9.7 DB2 10.1 PostgreSQL 9.1 Oracle 10.1 Oracle 10.2 Oracle 11.2
XMLELEMENT + + + + + + + +
XMLNAMESPACES + + + +
XMLATTRIBUTES + + + + + + +
XMLAGG + + + + + + + +
XMLFOREST + + + + + + + +
XMLCOLATTVAL + + +
XMLSEQUENCE + + +
XMLSERIALIZE + + + + + +
XMLCONCAT + + + + + + + +
XMLTABLE + + + + +
XMLPARSE + + + + +
XMLDOCUMENT + + +
XMLROOT + + +
XMLCOMMENT + + + + + +
XMLPI + + + + + +
XMLCDATA + +
XMLQUERY + + + + +
EXTRACT + + +
UPDATEXML + + +
XMLTEXT + + +
XMLVALIDATE + + +
XMLXSROBJECTID + + +
XMLGROUP + +
XMLROW + +
XSLTRANSFORM + +
XMLTRANSFORM + + +
XPATH +
EXISTSNODE + + +
XMLEXISTS + + + + +
XMLISVALID +
XPATH_EXISTS +
IS DOCUMENT +
xml_is_well_formed +
XMLTYPE + + +

I’m sorry for possible mistakes in this table it took hard googling to create comparison like this, if you find any inaccuracies leave the comment and I will fix it.

Resources: