Composing An XML Document From Relational Data: Part 2
August 22, 2012 Michael Sansoterra
In Part 1 of this series, I covered how to transform the results of a DB2 for i7.1 query into either an element- or attribute-based XML document. This time, I’m going to delve into a more complicated XML composition that requires the nesting of sales order XML elements as children of retail store elements. As with the last tip, I’ll start with a base SQL statement that extracts the required data and then gradually transform that statement until it produces the desired XML document using the new DB2 for i XML functionality. To start, here is the base query that extracts store information and the corresponding sales orders for each store: -- Base Query SELECT C.CUSTOMERID,C.ACCOUNTNUMBER,S.NAME, SALESORDERID AS "Id", CAST(SHIPDATE AS DATE) AS "ShipDate", TOTALDUE AS "Total" FROM ADVWORKS.CUSTOMER C JOIN ADVWORKS.STORE S ON S.CUSTOMERID=C.CUSTOMERID JOIN ADVWORKS.SALESORDERHEADER SOH ON SOH.CUSTOMERID=S.CUSTOMERID WHERE C.CUSTOMERTYPE='S'; Author’s Note: By the way, as usual, I’m using the Microsoft Adventure Works sample data for this demonstration. The shown code below, which we’ll call Figure 1, is what the final XML should look like: <Stores> <Store> <Account>AW00000001</Account> <Name>A Bike Store</Name> <Orders> <Order Id="43860" ShipDate="2001-08-08" Total="14603.7393" /> <Order Id="44501" ShipDate="2001-11-08" Total="26128.8674" /> <Order Id="45283" ShipDate="2002-02-08" Total="37643.1378" /> <Order Id="46042" ShipDate="2002-05-08" Total="34722.9906" /> </Orders> </Store> <Store> <Account>AW00000002</Account> <Name>Progressive Sports</Name> <Orders> <Order Id="46976" ShipDate="2002-08-08" Total="10184.0774" /> <Order Id="47997" ShipDate="2002-11-08" Total="5469.5941" /> <Order Id="49054" ShipDate="2003-02-08" Total="1739.4078" /> <Order Id="50216" ShipDate="2003-05-08" Total="1935.5166" /> <Order Id="51728" ShipDate="2003-08-08" Total="3905.2547" /> <Order Id="57044" ShipDate="2003-11-08" Total="4537.8484" /> <Order Id="63198" ShipDate="2004-02-08" Total="4053.9506" /> <Order Id="69488" ShipDate="2004-05-08" Total="908.3199" /> </Orders> </Store> </Stores> Figure 1. Final XML containing store names (element based) and the respective orders (attribute based.) If you haven’t read Part 1, please review it so you understand the basics of the new i7.1 XML functions. The trick with this example is to nest the <Order> elements as children of the <Store> elements. To ease the XML construction task, I’m going to restructure the query using a common table expression (CTE) for the store data. Hopefully it will become clear as to why I’m doing this as you read on: -- Step 1 - Aggregate the sales order XML at the store level WITH CTE_STORES AS ( SELECT C.CUSTOMERID,C.ACCOUNTNUMBER,S.NAME FROM ADVWORKS.CUSTOMER C JOIN ADVWORKS.STORE S ON S.CUSTOMERID=C.CUSTOMERID WHERE C.CUSTOMERTYPE='S' ) SELECT S.CUSTOMERID,S.ACCOUNTNUMBER,S.NAME, XMLAGG(XMLROW(SALESORDERID AS "Id",CAST(SHIPDATE AS DATE) AS "ShipDate",TOTALDUE AS "Total" OPTION ROW "Order" AS ATTRIBUTES)) AS XML_ORDER_DATA FROM CTE_STORES S JOIN ADVWORKS.SALESORDERHEADER SOH ON SOH.CUSTOMERID=S.CUSTOMERID GROUP BY S.CUSTOMERID,S.ACCOUNTNUMBER,S.NAME XMLROW is used to convert the sales order data into an attribute-based XML fragment (one fragment for each sales order) with <Order> as the parent element. The XMLAGG aggregate function is used to concatenate all of the sales order fragments for each store (as specified in the GROUP BY). The abridged query results look like this (with modified heading names for the sake of space):
The result has a row for each store, and the XML_ORDER_DATA column contains the XML for each store’s sales orders. Next, we’ll need to transform each store’s information into an XML <Store> element, and nest the sales orders as its children. This can be accomplished once again by using the XML row function: -- Step 2 - Transform the store columns into XML elements WITH CTE_STORES AS ( SELECT C.CUSTOMERID,C.ACCOUNTNUMBER,S.NAME FROM ADVWORKS.CUSTOMER C JOIN ADVWORKS.STORE S ON S.CUSTOMERID=C.CUSTOMERID WHERE C.CUSTOMERTYPE='S' ) SELECT XMLROW(S.ACCOUNTNUMBER AS "Account",S.NAME AS "Name", XMLAGG(XMLROW(SALESORDERID AS "Id",CAST(SHIPDATE AS DATE) AS "ShipDate",TOTALDUE AS "Total" OPTION ROW "Order" AS ATTRIBUTES)) AS "Orders" OPTION ROW "Store") AS XML_Scalar FROM CTE_STORES S JOIN ADVWORKS.SALESORDERHEADER SOH ON SOH.CUSTOMERID=S.CUSTOMERID GROUP BY S.CUSTOMERID,S.ACCOUNTNUMBER,S.NAME The outer XMLROW function accomplishes the following:
The results of the query now look like this:
That is getting pretty close to the final product shown above in Figure 1. (Remember all line breaks were inserted for readability). To achieve the final product, there are only two steps left. The first step is to aggregate the <Store> fragments into a single XML column. The second step is to assign a root element called <Stores>. To make things easier to read, I placed the prior query example into a new CTE called CTE_STORES_ORDERS which returns multiple rows and one column called XML_DATA. The main SELECT will now operate on this one column and concatenate all of the rows into a single value using XMLAGG: -- Step 3 -- Aggregate XML at the store level WITH CTE_STORES AS ( SELECT C.CUSTOMERID,C.ACCOUNTNUMBER,S.NAME FROM ADVWORKS.CUSTOMER C JOIN ADVWORKS.STORE S ON S.CUSTOMERID=C.CUSTOMERID WHERE C.CUSTOMERTYPE='S' ), CTE_STORES_ORDERS AS ( SELECT XMLROW(S.ACCOUNTNUMBER AS "Account",S.NAME AS "Name", XMLAGG(XMLROW(SALESORDERID AS "Id",CAST(SHIPDATE AS DATE) AS "ShipDate",TOTALDUE AS "Total" OPTION ROW "Order" AS ATTRIBUTES)) AS "Orders" OPTION ROW "Store") AS XML_Data FROM CTE_STORES S JOIN ADVWORKS.SALESORDERHEADER SOH ON SOH.CUSTOMERID=S.CUSTOMERID GROUP BY S.CUSTOMERID,S.ACCOUNTNUMBER,S.NAME ) SELECT XMLDOCUMENT(XMLELEMENT(NAME "Stores",XMLAGG(XML_Data))) AS XML_Document FROM CTE_STORES_ORDERS Function XMLAGG(XML_Data) combines all of the XML from the prior result set into a single XML column. This aggregate expression is the second parameter to the XMLELEMENT function. XMLELEMENT creates the root <Stores> element and assigns the aggregated store and order XML as its value. The XMLDOCUMENT function is used to validate that the XML has been constructed correctly (a valid XML document with one root element). The results of the DB2 for i query now match the XML shown in Figure 1. The XML capabilities in i7.1 are marvelous. Of course there is more than one way to skin a cat. There are several other new XML functions in DB2 for i7.1 that I haven’t introduced that can be used to write alternative queries yet produce the same results. Check them out in the DB2 for i SQL Reference. DB2 for i is a great tool for composing XML documents based on data in your relational database. Michael Sansoterra is a DBA for Broadway Systems in Grand Rapids, Michigan. Send your questions or comments for Mike via the IT Jungle Contact page. RELATED STORY Composing An XML Document From Relational Data, Part 1
|