PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Fonction Postgres renvoyant une ligne en tant que valeur JSON

Je vois deux problèmes majeurs :
1. Vous ne pouvez pas mettre de UPDATE dans une sous-requête du tout . Vous pouvez résoudre ce problème avec un modification des données CTE comme Patrick démontre , mais cela coûte plus cher et est plus verbeux que nécessaire pour le cas en question.
2. Vous avez un conflit de nom potentiellement dangereux , cela n'a pas encore été résolu.

Meilleure requête/fonction

Laissant de côté le wrapper de la fonction SQL pour le moment (nous y reviendrons). Vous pouvez utiliser une simple UPDATE avec un RETURNING clause :

UPDATE tbl
SET    value1 = 'something_new'
WHERE  id = 123
RETURNING row_to_json(ROW(value1, value2));

Le RETURNING La clause autorise des expressions arbitraires impliquant des colonnes de la ligne mise à jour. C'est plus court et moins cher qu'un CTE de modification de données.

Le problème restant :le constructeur de ligne ROW(...) ne conserve pas les noms de colonnes (ce qui est une faiblesse connue), vous obtenez donc des clés génériques dans votre valeur JSON :

row_to_json
{"f1":"something_new","f2":"what ever is in value2"}

Dans Postgres 9.3, vous auriez besoin d'une autre fonction CTE pour encapsuler la première étape ou une conversion en un type de ligne bien défini. Détails :

Dans Postgres 9.4 utilisez simplement json_build_object() ou json_object() :

UPDATE tbl
SET    value1 = 'something_new'
WHERE  id = 123
RETURNING json_build_object('value1', value1, 'value2', value2);

Ou :

...
RETURNING json_object('{value1, value2}', ARRAY[value1, value2]);

Vous obtenez maintenant les noms de colonne d'origine ou tout ce que vous avez choisi comme noms de clé :

row_to_json
{"value1":"something_new","value2":"what ever is in value2"}

Il est facile d'envelopper cela dans une fonction, ce qui nous amène à votre deuxième problème...

Conflit de nom

Dans votre fonction d'origine, vous utilisez des noms identiques pour les paramètres de fonction et les noms de colonne. C'est généralement une très mauvaise idée . Vous auriez besoin de comprendre intimement quel identifiant vient en premier dans quelle portée.

Dans le cas présent, le résultat est un non-sens total :

Create Or Replace Function ExampleTable_Update (id bigint, value1 text) Returns 
...
    Update ExampleTable
    Set Value1 = value1
    Where id = id
    Returning Value1, Value2;
...
$$ Language SQL;

Alors que vous semblez vous attendre à ce que la deuxième instance de id ferait référence au paramètre de fonction, ce n'est pas le cas. Le nom de la colonne vient en premier dans la portée d'une instruction SQL, la deuxième instance fait référence à la colonne. résultant en une expression qui est toujours true sauf pour les valeurs NULL dans id . Par conséquent, vous mettriez à jour toutes les lignes , ce qui pourrait entraîner une perte catastrophique de données . Pire encore, vous ne vous en rendrez peut-être même pas compte plus tard, car la fonction SQL renverra un ligne arbitraire telle que définie par le RETURNING clause de la fonction (renvoie un ligne, pas un ensemble de lignes).

Dans ce cas particulier, vous auriez de la "chance", car vous avez également value1 = value1 , qui écrase la colonne avec sa valeur préexistante, ne faisant effectivement .. rien d'une manière très coûteuse (à moins que les déclencheurs ne fassent quelque chose). Vous pourriez être surpris d'obtenir une ligne arbitraire avec une value1 inchangée comme résultat.

Alors, ne le faites pas.

Évitez les conflits de nommage potentiels comme celui-ci à moins que vous ne sachiez exactement ce que vous faites (ce qui n'est évidemment pas le cas). Une convention que j'aime consiste à ajouter un trait de soulignement pour les noms de paramètres et de variables dans les fonctions, tandis que les noms de colonne ne commencent jamais par un trait de soulignement. Dans de nombreux cas, vous pouvez simplement utiliser des références de position pour être sans ambiguïté :$1 , $2 , ..., mais cela n'élude qu'une moitié du problème. N'importe quelle méthode est bonne tant que vous évitez les conflits de noms . Je suggère :

CREATE OR REPLACE FUNCTION foo (_id bigint, _value1 text)
   RETURNS json AS
$func$
UPDATE tbl
SET    value1 = _value1
WHERE  id     = _id
RETURNING json_build_object('value1', value1, 'value2', value2);
$func$  LANGUAGE sql;

Notez également que cela renvoie la valeur réelle de la colonne dans value1 après la UPDATE , qui peut ou non être le même que votre paramètre d'entrée _value1 . Il pourrait y avoir des règles de base de données ou des déclencheurs interférant ...