Joe DiNottra
Joe DiNottra

Reputation: 1018

How do I produce an XML response from PostgreSQL 1:n tables?

I have the table "client" with:

id  name     registered_on  status
--  -------  -------------  ------
 1  Alice    2020-03-04     a     
 2  Vincent  2020-03-05     p     
 3  Anne     2020-03-06     a     

And the table "account" with:

client_id  account_number  type  balance
---------  --------------  ----  -------
        1  300-1           CHK       100
        2  307-5           SAV        24
        2  307-6           CHK       350

I created them in DB Fiddle (for a similar question I asked before about producing JSON).

Now, I need a SQL query to produce the 1:n XML document:

<client id="1" name="Alice" registered_on="2020-03-04" status="a">
  <account account_number="300-1" type="CHK" balance="100" />
</client>

<client id="2" name="Vincent" registered_on="2020-03-05" status="p">
  <account account_number="307-5" type="SAV" balance="24" />
  <account account_number="307-6" type="CHK" balance="350" />
</client>

<client id="3" name="Anne" registered_on="2020-03-06" status="a" />

There's a 1:n relationship between the tables and some clients may not have an account (such as "Anne"). The result is a simple join (probably an outer join) that I know how to do. I just don't get how to produce a XML document from it.

If it's makes it easier/shorter I'm open to an alternative XML result, as long as it represents the same data; using tags, instead of attributes, for example.

Upvotes: 1

Views: 240

Answers (1)

Joe DiNottra
Joe DiNottra

Reputation: 1018

After trying a bunch of options I was able to find the answer(s).

Original Format: With Attributes

It's possible to produce the XML result using an outer join:

select
  xmlserialize(content -- remove this line to keep as XML instead of VARCHAR
  xmlagg(r)
  as text) -- remove this line to keep as XML instead of VARCHAR
from (
  select
    xmlelement(name client,
      xmlattributes(c.id, c.name, c.registered_on, c.status),
      case when count(a.client_id) > 0 then 
        xmlagg(xmlelement(name account, 
                 xmlattributes(a.account_number, a.type, a.balance) ))
      end
    ) as r
  from client c
  left join account a on a.client_id = c.id
  group by c.id
) s

Or using subqueries (shorter but less performant):

select
  xmlserialize(content -- remove this line to keep as XML instead of VARCHAR
  xmlagg(
  xmlelement(name client, xmlattributes(id, name, registered_on, status),
    ( select xmlagg(xmlelement(name account,
             xmlattributes(a.account_number, a.type, a.balance)
      )) from account a where a.client_id = c.id
    )
  ))
  as text) -- remove this line to keep as XML instead of VARCHAR
from client c;

Result:

<client id="1" name="Alice" registered_on="2020-03-04" status="a">
  <account account_number="300-1" type="CHK" balance="100.00" />
</client>
<client id="2" name="Vincent" registered_on="2020-03-05" status="p">
  <account account_number="307-5" type="SAV" balance="24.00" />
  <account account_number="307-6" type="CHK" balance="350.00" />
</client>
<client id="3" name="Anne" registered_on="2020-03-06" status="a" />

Alternative Format: Without Attributes

Some people prefer to avoid attributes altogether and always use tags. That can also be done, using:

select 
  xmlserialize(content -- remove this line to keep as XML instead of VARCHAR
  xmlagg(xmlelement(name client,
    xmlforest(id, name, registered_on, status), 
    ( select xmlagg(xmlelement(name account, 
             xmlforest(a.account_number, a.type, a.balance))) 
      from account a where a.client_id = c.id 
    )
  ))
  as text) -- remove this line to keep as XML instead of VARCHAR
from client c;

Result:

<client>
  <id>1</id>
  <name>Alice</name>
  <registered_on>2020-03-04</registered_on>
  <status>a</status>
  <account>
    <account_number>300-1</account_number>
    <type>CHK</type>
    <balance>100.00</balance>
  </account>
</client>
<client>
  <id>2</id>
  <name>Vincent</name>
  <registered_on>2020-03-05</registered_on>
  <status>p</status>
  <account>
    <account_number>307-5</account_number>
    <type>SAV</type>
    <balance>24.00</balance>
  </account>
  <account>
    <account_number>307-6</account_number>
    <type>CHK</type>
    <balance>350.00</balance>
  </account>
</client>
<client>
  <id>3</id>
  <name>Anne</name>
  <registered_on>2020-03-06</registered_on>
  <status>a</status>
</client>

Upvotes: 1

Related Questions