J'essaie de mapper les résultats d'une requête à JSON à l'aide de la fonction row_to_json()
ajoutée dans PostgreSQL 9.2.
Je n'arrive pas à trouver le meilleur moyen de représenter les lignes jointes sous forme d'objets imbriqués (relations 1: 1)
Voici ce que j'ai essayé (code d'installation: tables, exemples de données, suivi d'une requête):
-- 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', '[email protected]', role_id);
END$$;
La requête elle-même:
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;
J'ai trouvé que si j'utilisais ROW()
, je pouvais séparer les champs résultants en un objet enfant, mais cela semble limité à un seul niveau. Je ne peux pas insérer plus d'instructions AS XXX
, Car je pense que je devrais en avoir besoin dans ce cas.
Les noms de colonne me sont attribués, car j’ai converti le type d’enregistrement approprié, par exemple avec ::user_roles
, Dans le cas des résultats de cette table.
Voici ce que cette requête renvoie:
{
"id":1,
"name":"Dan",
"email":"[email protected]",
"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"
}
}
}
}
Ce que je veux faire, c'est générer du JSON pour les jointures (encore une fois, 1: 1 c'est bien) d'une manière qui permette d'ajouter des jointures et de les faire représenter en tant qu'objets enfants des parents auxquels elles se joignent, comme ci-dessous:
{
"id":1,
"name":"Dan",
"email":"[email protected]",
"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"
}
}
}
}
Toute aide est appréciée. Merci d'avoir lu.
Mise à jour: Dans PostgreSQL 9.4, cela s’améliore beaucoup avec l’introduction de to_json
, json_build_object
, json_object
Et json_build_array
, bien que ce soit verbose en raison de la nécessité de nommer explicitement tous les champs:
select
json_build_object(
'id', u.id,
'name', u.name,
'email', u.email,
'user_role_id', u.user_role_id,
'user_role', json_build_object(
'id', ur.id,
'name', ur.name,
'description', ur.description,
'duty_id', ur.duty_id,
'duty', json_build_object(
'id', d.id,
'name', d.name
)
)
)
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;
Pour les anciennes versions, lisez la suite.
Ce n'est pas limité à une seule ligne, c'est juste un peu douloureux. Vous ne pouvez pas alias les types de lignes composites en utilisant AS
, vous devez donc utiliser une expression de sous-requête avec alias ou un CTE pour obtenir l'effet suivant:
select row_to_json(row)
from (
select u.*, urd AS user_role
from users u
inner join (
select ur.*, d
from user_roles ur
inner join role_duties d on d.id = ur.duty_id
) urd(id,name,description,duty_id,duty) on urd.id = u.user_role_id
) row;
produit, via http://jsonprettyprint.com/ :
{
"id": 1,
"name": "Dan",
"email": "[email protected]",
"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"
}
}
}
Vous voudrez utiliser array_to_json(array_agg(...))
lorsque vous avez une relation 1: plusieurs, d'ailleurs.
La requête ci-dessus devrait idéalement pouvoir être écrite comme suit:
select row_to_json(
ROW(u.*, ROW(ur.*, d AS duty) 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;
... mais le constructeur ROW
de PostgreSQL n'accepte pas les alias de colonnes AS
. Tristement.
Heureusement, ils optimisent les mêmes. Comparez les plans:
ROW
du constructeur avec les alias supprimés afin qu’il soit exécutéÉtant donné que les CTE sont des barrières d’optimisation, la reformulation de la version de la sous-requête imbriquée afin d’utiliser des CTE chaînés (expressions WITH
) risque de ne pas être aussi performante et d’aboutir au même plan. Dans ce cas, vous êtes en quelque sorte coincé avec des sous-requêtes imbriquées jusqu'à ce que nous obtenions quelques améliorations à row_to_json
Ou un moyen de remplacer plus directement les noms de colonnes dans un constructeur ROW
.
Quoi qu’il en soit, en général, le principe est celui où vous voulez créer un objet json avec des colonnes a, b, c
, Et vous souhaitez pouvoir écrire simplement la syntaxe illégale:
ROW(a, b, c) AS outername(name1, name2, name3)
vous pouvez plutôt utiliser des sous-requêtes scalaires renvoyant des valeurs de type ligne:
(SELECT x FROM (SELECT a AS name1, b AS name2, c AS name3) x) AS outername
Ou:
(SELECT x FROM (SELECT a, b, c) AS x(name1, name2, name3)) AS outername
De plus, gardez à l’esprit que vous pouvez composer json
valeurs sans autre guillemet, par ex. si vous placez la sortie d'un json_agg
dans un row_to_json
, le résultat interne json_agg
ne sera pas cité sous forme de chaîne, il sera incorporé directement sous forme json.
par exemple. dans l'exemple arbitraire:
SELECT row_to_json(
(SELECT x FROM (SELECT
1 AS k1,
2 AS k2,
(SELECT json_agg( (SELECT x FROM (SELECT 1 AS a, 2 AS b) x) )
FROM generate_series(1,2) ) AS k3
) x),
true
);
la sortie est:
{"k1":1,
"k2":2,
"k3":[{"a":1,"b":2},
{"a":1,"b":2}]}
Notez que le produit json_agg
, [{"a":1,"b":2}, {"a":1,"b":2}]
, N'a pas été échappé à nouveau, comme le serait text
.
Cela signifie que vous pouvez composer des opérations JSON pour construire des lignes, vous n'avez pas toujours besoin de créer des types composites PostgreSQL extrêmement complexes, puis appelez row_to_json
. sur la sortie.
Ma suggestion pour la maintenabilité à long terme est d'utiliser une vue pour construire la version simplifiée de votre requête, puis utiliser une fonction comme ci-dessous:
CREATE OR REPLACE FUNCTION fnc_query_prominence_users( )
RETURNS json AS $$
DECLARE
d_result json;
BEGIN
SELECT ARRAY_TO_JSON(
ARRAY_AGG(
ROW_TO_JSON(
CAST(ROW(users.*) AS prominence.users)
)
)
)
INTO d_result
FROM prominence.users;
RETURN d_result;
END; $$
LANGUAGE plpgsql
SECURITY INVOKER;
Dans ce cas, l'objet prominence.users est une vue. Depuis que j'ai sélectionné des utilisateurs. *, Je n'aurai pas à mettre à jour cette fonction si je dois mettre à jour la vue pour inclure plus de champs dans un enregistrement d'utilisateur.