I’m trying to map the results of a query to JSON using the row_to_json() function that was added in PostgreSQL 9.2.
I’m having trouble figuring out the best way to represent joined rows as nested objects (1:1 relations)
Here’s what I’ve tried (setup code: tables, sample data, followed by query):
-- some test tables to start out with:
create table role_duties (
id serial primary key,
name varchar
);
create table user_roles (
id serial primary key,
name varchar,
description varchar,
duty_id int, foreign key (duty_id) references role_duties(id)
);
create table users (
id serial primary key,
name varchar,
email varchar,
user_role_id int, foreign key (user_role_id) references user_roles(id)
);
DO $$
DECLARE duty_id int;
DECLARE role_id int;
begin
insert into role_duties (name) values ('Script Execution') returning id into duty_id;
insert into user_roles (name, description, duty_id) values ('admin', 'Administrative duties in the system', duty_id) returning id into role_id;
insert into users (name, email, user_role_id) values ('Dan', 'someemail@gmail.com', role_id);
END$$;
The query itself:
select row_to_json(row)
from (
select u.*, ROW(ur.*::user_roles, ROW(d.*::role_duties)) as user_role
from users u
inner join user_roles ur on ur.id = u.user_role_id
inner join role_duties d on d.id = ur.duty_id
) row;
I found if I used ROW(), I could separate the resulting fields out into a child object, but it seems limited to a single level. I can’t insert more AS XXX statements, as I think I should need in this case.
I am afforded column names, because I cast to the appropriate record type, for example with ::user_roles, in the case of that table’s results.
Here’s what that query returns:
{
"id":1,
"name":"Dan",
"email":"someemail@gmail.com",
"user_role_id":1,
"user_role":{
"f1":{
"id":1,
"name":"admin",
"description":"Administrative duties in the system",
"duty_id":1
},
"f2":{
"f1":{
"id":1,
"name":"Script Execution"
}
}
}
}
What I want to do is generate JSON for joins (again 1:1 is fine) in a way where I can add joins, and have them represented as child objects of the parents they join to, i.e. like the following:
{
"id":1,
"name":"Dan",
"email":"someemail@gmail.com",
"user_role_id":1,
"user_role":{
"id":1,
"name":"admin",
"description":"Administrative duties in the system",
"duty_id":1
"duty":{
"id":1,
"name":"Script Execution"
}
}
}
}
Update: In PostgreSQL 9.4 this improves a lot with the introduction of
to_json,json_build_object,json_objectandjson_build_array, though it’s verbose due to the need to name all the fields explicitly:For older versions, read on.
It isn’t limited to a single row, it’s just a bit painful. You can’t alias composite rowtypes using
AS, so you need to use an aliased subquery expression or CTE to achieve the effect:produces, via http://jsonprettyprint.com/:
You will want to use
array_to_json(array_agg(...))when you have a 1:many relationship, btw.The above query should ideally be able to be written as:
… but PostgreSQL’s
ROWconstructor doesn’t acceptAScolumn aliases. Sadly.Thankfully, they optimize out the same. Compare the plans:
ROWconstructor version with the aliases removed so it executesBecause CTEs are optimisation fences, rephrasing the nested subquery version to use chained CTEs (
WITHexpressions) may not perform as well, and won’t result in the same plan. In this case you’re kind of stuck with ugly nested subqueries until we get some improvements torow_to_jsonor a way to override the column names in aROWconstructor more directly.Anyway, in general, the principle is that where you want to create a json object with columns
a, b, c, and you wish you could just write the illegal syntax:you can instead use scalar subqueries returning row-typed values:
Or:
Additionally, keep in mind that you can compose
jsonvalues without additional quoting, e.g. if you put the output of ajson_aggwithin arow_to_json, the innerjson_aggresult won’t get quoted as a string, it’ll be incorporated directly as json.e.g. in the arbitrary example:
the output is:
Note that the
json_aggproduct,[{"a":1,"b":2}, {"a":1,"b":2}], hasn’t been escaped again, astextwould be.This means you can compose json operations to construct rows, you don’t always have to create hugely complex PostgreSQL composite types then call
row_to_jsonon the output.