Re: Best way to populate nested composite type from JSON` - Mailing list pgsql-general

From Merlin Moncure
Subject Re: Best way to populate nested composite type from JSON`
Date
Msg-id CAHyXU0x3iLjff8wMuyneRiLMGdtWxajM-BwLT7HZePCGcMzf0Q@mail.gmail.com
Whole thread Raw
In response to Re: Best way to populate nested composite type from JSON`  (Chris Travers <chris.travers@gmail.com>)
Responses Re: Best way to populate nested composite type from JSON`
List pgsql-general
On Thu, Sep 12, 2013 at 7:25 PM, Chris Travers <chris.travers@gmail.com> wrote:
>
>
>
> On Thu, Sep 12, 2013 at 8:47 AM, Merlin Moncure <mmoncure@gmail.com> wrote:
>>
>>
>>
>> Can we see a hypothetical example?  json best practices for me are to
>> use standard tables and than serialize/deserialize json as it goes
>> through the door and not use nested composite types in your actual
>> table definitions.  I think it should all fit if you do it right: you
>> json_[each/array_elements] the wrapping json then insert it into
>> un-nested actual tables.  If you must have a final destination as a
>> nested composite, I think at the worst case you might have to make
>> some transition composites...
>
>
> Here is what I am thinking (roughly):
>
> CREATE TYPE trans_line (
>      description text,
>      amount numeric,
>      account_id int
> );
>
> CREATE TYPE journal_entry (
>       reference text,
>       description text,
>       post_date date,
>       lines trans_line[]
> );
>
> CREATE FUNCTION save(journal_entry) RETURNS .... LANGUAGE PLPGSQL AS
> $$
> ....
> $$;
>
> What I would like to be able to do is accept that complex data type in as a
> JSON object and convert it to the record format.  The data could then be
> broken apart, inserted checked for validity (make sure the transaction is
> balanced etc) and then inserted into the correct tables.  My idea was to
> create a json::journal_entry cast and use that.  Otherwise I get to have fun
> with row and array constructors and system tables.
>
> However one can't pass nested JSON objects through json_populate_record().
> My question is basically how to get around that.

It would certainly be better if we could straight up deserialize json
into a nested structure.  For now, my advise is to try and structure
your json and the receiving types/tables to not be nested.  Using your
example, I was able to do that by breaking the object in the json and
eploiting hte 'best effort' json->object mapping (if you wanted to
support receiving more than one entry at a time, you would need to
include relation information to hook journal_entry to trans_lines.

First, let's state the problem:
CREATE TYPE trans_line AS (
     description text,
     amount numeric,
     account_id int
);

CREATE TYPE journal_entry AS(
      reference text,
      description text,
      post_date date,
      lines trans_line[]
);

/* make some test data */
postgres=# SELECT row('a', 'b', now(), array[row('c', 1.0, 2),
row('d', 3.0, 4)]::trans_line[])::journal_entry;
                       row
--------------------------------------------------
 (a,b,2013-09-13,"{""(c,1.0,2)"",""(d,3.0,4)""}")


/* uh oh */
postgres=# select json_populate_record(null::journal_entry,
row_to_json(row('a', 'b', now(), array[row('c', 1.0, 2), row('d', 3.0,
4)]::trans_line[])::journal_entry));
ERROR:  cannot call json_populate_record on a nested object

/* json -> record mapping is 'best effort' */
postgres=# select json_populate_record(null::journal_entry, '{}');
 json_populate_record
----------------------
 (,,,)

/* so, push the entry and the lines into sub-elements of a wrapping
object, de-serialize separately and map back together. */


WITH in_json AS
(
  SELECT row_to_json(q) AS data
  FROM
  (
    SELECT
      q::journal_entry AS journal_entries,
      ARRAY[ROW('c', 1.0, 2), ROW('d', 3.0, 4)]::trans_line[] AS trans_lines
    FROM
    (
      SELECT
        'a',
        'b',
        now(),
        null::trans_line[]
    ) q
  ) q
),
je AS
(
  SELECT
    q.*
  FROM in_json
  CROSS JOIN LATERAL
    json_populate_record(null::journal_entry, data->'journal_entries') q
),
tl AS
(
  SELECT
    q.*
  FROM in_json
  CROSS JOIN LATERAL
    json_populate_recordset(null::trans_line, data->'trans_lines') q
)
SELECT je.*, ARRAY(SELECT tl FROM tl) AS lines2 FROM je;

 reference | description | post_date  | lines |          lines2
-----------+-------------+------------+-------+---------------------------
 a         | b           | 2013-09-13 |       | {"(c,1.0,2)","(d,3.0,4)"}

that's somewhat tedious, but not too bad I think (the query above
looks longer than it would be in practice since the json construction
would presumably be on the client).  But the basic M.O. is to send
lists of records back to the server and relate them on the fly.

merlin


pgsql-general by date:

Previous
From: e-letter
Date:
Subject: e: Running/cumulative count using windows
Next
From: Tom Lane
Date:
Subject: Re: Major upgrade of PostgreSQL and MySQL