Comment effectuer des opérations de mise à jour sur des colonnes de type JSONB dans Postgres 9.4

133

En parcourant la documentation du JSONB de type de données Postgres 9.4, je ne vois pas immédiatement comment faire des mises à jour sur les colonnes JSONB.

Documentation pour les types et fonctions JSONB:

http://www.postgresql.org/docs/9.4/static/functions-json.html http://www.postgresql.org/docs/9.4/static/datatype-json.html

À titre d'exemple, j'ai cette structure de table de base:

CREATE TABLE test(id serial, data jsonb);

L'insertion est facile, comme dans:

INSERT INTO test(data) values ('{"name": "my-name", "tags": ["tag1", "tag2"]}');

Maintenant, comment mettre à jour la colonne «données»? Cette syntaxe n'est pas valide:

UPDATE test SET data->'name' = 'my-other-name' WHERE id = 1;

Est-ce documenté quelque part évident que j'ai manqué? Merci.

jvous
la source

Réponses:

33

Idéalement, vous n'utilisez pas de documents JSON pour des données structurées et régulières que vous souhaitez manipuler dans une base de données relationnelle. Utilisez plutôt une conception relationnelle normalisée .

JSON est principalement destiné à stocker des documents entiers qui n'ont pas besoin d'être manipulés dans le SGBDR. En relation:

La mise à jour d'une ligne dans Postgres écrit toujours une nouvelle version de la ligne entière . C'est le principe de base du modèle MVCC de Postgres . Du point de vue des performances, peu importe que vous modifiiez une seule donnée dans un objet JSON ou la totalité de celle-ci: une nouvelle version de la ligne doit être écrite.

Ainsi les conseils dans le manuel :

Les données JSON sont soumises aux mêmes considérations de contrôle d'accès concurrentiel que tout autre type de données lorsqu'elles sont stockées dans une table. Bien que le stockage de documents volumineux soit possible, gardez à l'esprit que toute mise à jour acquiert un verrou au niveau de la ligne sur toute la ligne. Envisagez de limiter les documents JSON à une taille gérable afin de réduire les conflits de verrouillage entre les transactions de mise à jour. Idéalement, les documents JSON devraient chacun représenter une donnée atomique que les règles métier dictent ne peut raisonnablement pas être subdivisée en plus petites références qui pourraient être modifiées indépendamment.

L'essentiel: pour modifier quoi que ce soit à l' intérieur d'un objet JSON, vous devez affecter un objet modifié à la colonne. Postgres fournit des moyens limités pour créer et manipuler des jsondonnées en plus de ses capacités de stockage. L'arsenal d'outils s'est considérablement développé avec chaque nouvelle version depuis la version 9.2. Mais le principal demeure: vous devez toujours affecter un objet complètement modifié à la colonne et Postgres écrit toujours une nouvelle version de ligne pour toute mise à jour.

Quelques techniques comment travailler avec les outils de Postgres 9.3 ou version ultérieure:

Cette réponse a attiré autant de votes négatifs que toutes mes autres réponses sur les SO ensemble . Les gens ne semblent pas aimer l'idée: une conception normalisée est supérieure pour les données non dynamiques. Cet excellent article de blog de Craig Ringer explique plus en détail:

Erwin Brandstetter
la source
6
Cette réponse ne concerne que le type JSON et ignore JSONB.
fiatjaf
7
@fiatjaf: Cette réponse est pleinement applicable aux types de données jsonet autres jsonb. Les deux stockent les données JSON, le jsonbfont sous une forme binaire normalisée qui présente certains avantages (et quelques inconvénients). stackoverflow.com/a/10560761/939860 Aucun des deux types de données n'est bon pour manipuler beaucoup dans la base de données. Aucun type de document n'est. Eh bien, c'est bien pour les petits documents JSON à peine structurés. Mais de gros documents imbriqués seraient une folie de cette façon.
Erwin Brandstetter
7
"Instructions comment travailler avec les outils de Postgres 9.3" devrait vraiment être le premier dans votre réponse car il répond à la question posée .. il est parfois judicieux de mettre à jour json pour des modifications de maintenance / de schéma, etc. 't really apply
Michael Wasser
22
Cette réponse n'est pas utile, désolé. @jvous, tu ne veux pas accepter la réponse de Jimothy, car elle répond vraiment à ta question?
Bastian Voigt
10
Répondez à la question avant d'ajouter votre propre commentaire / opinion / discussion.
Ppp le
332

Si vous pouvez mettre à niveau vers Postgresql 9.5, la jsonb_setcommande est disponible, comme d'autres l'ont mentionné.

Dans chacune des instructions SQL suivantes, j'ai omis la whereclause par souci de concision; évidemment, vous voudriez l'ajouter.

Mettre à jour le nom:

UPDATE test SET data = jsonb_set(data, '{name}', '"my-other-name"');

Remplacez les balises (par opposition à l'ajout ou à la suppression de balises):

UPDATE test SET data = jsonb_set(data, '{tags}', '["tag3", "tag4"]');

Remplacement de la deuxième balise (indexée 0):

UPDATE test SET data = jsonb_set(data, '{tags,1}', '"tag5"');

Ajouter une balise ( cela fonctionnera tant qu'il y aura moins de 999 balises; changer l'argument 999 en 1000 ou plus génère une erreur . Cela ne semble plus être le cas dans Postgres 9.5.3; un index beaucoup plus grand peut être utilisé) :

UPDATE test SET data = jsonb_set(data, '{tags,999999999}', '"tag6"', true);

Supprimez la dernière balise:

UPDATE test SET data = data #- '{tags,-1}'

Mise à jour complexe (supprimez la dernière balise, insérez une nouvelle balise et modifiez le nom):

UPDATE test SET data = jsonb_set(
    jsonb_set(data #- '{tags,-1}', '{tags,999999999}', '"tag3"', true), 
    '{name}', '"my-other-name"');

Il est important de noter que dans chacun de ces exemples, vous ne mettez pas réellement à jour un seul champ des données JSON. Au lieu de cela, vous créez une version temporaire et modifiée des données et attribuez cette version modifiée à la colonne. En pratique, le résultat devrait être le même, mais en gardant cela à l'esprit, les mises à jour complexes, comme le dernier exemple, devraient être plus compréhensibles.

Dans l'exemple complexe, il existe trois transformations et trois versions temporaires: Premièrement, la dernière balise est supprimée. Ensuite, cette version est transformée en ajoutant une nouvelle balise. Ensuite, la deuxième version est transformée en modifiant le namechamp. La valeur de la datacolonne est remplacée par la version finale.

Jimothy
la source
42
vous obtenez des points bonus pour montrer comment mettre à jour une colonne dans une table comme l'OP l'a demandé
chadrik
1
@chadrik: J'ai ajouté un exemple plus complexe. Cela ne fait pas exactement ce que vous avez demandé, mais cela devrait vous donner une idée. Notez que l'entrée de l' jsonb_setappel externe est la sortie de l'appel interne et que l'entrée de cet appel interne est le résultat de data #- '{tags,-1}'. C'est-à-dire les données d'origine avec la dernière balise supprimée.
Jimothy
1
@PranaySoni: Pour cela, j'utiliserais probablement une procédure stockée ou, si la surcharge n'est pas un problème, ramènerais ces données, les manipulerais dans le langage de l'application, puis les réécrirais. Cela semble lourd, mais gardez à l'esprit que dans tous les exemples que j'ai donnés, vous ne mettez toujours pas à jour un seul champ dans le JSON (B): vous écrasez toute la colonne de toute façon. Donc, un proc stocké n'est pas vraiment différent.
Jimothy
1
@Alex: Oui, un peu hack. Si je disais {tags,0}, cela signifierait "le premier élément du tableau tags", me permettant de donner une nouvelle valeur à cet élément. En utilisant un grand nombre au lieu de 0, au lieu de remplacer un élément existant dans le tableau, il ajoute un nouvel élément au tableau. Cependant, si le tableau contenait en fait plus de 999 999 999 éléments, cela remplacerait le dernier élément au lieu d'en ajouter un nouveau.
Jimothy
1
qu'en est-il si le champ contient null? les regards ne fonctionnent pas. Par exemple, le champ info jsonb est nul: "UPDATE organizer SET info = jsonb_set (info, '{country}', '" FRA "') where info - >> 'country' :: text IS NULL;" J'obtiens un enregistrement UPDATE 105 mais pas de changements sur db
stackdave
24

Cela arrive en 9.5 sous la forme de jsonb_set par Andrew Dunstan basé sur une extension existante jsonbx qui fonctionne avec 9.4

philofinfinitejest
la source
Un autre problème dans cette ligne, est l'utilisation de jsonb_build_object(), car x->key, ne retourne pas la paire clé-objet, pour remplir dont vous avez besoin jsonb_set(target, path, jsonb_build_object('key',x->key)).
Peter Krauss
19

Pour ceux qui rencontrent ce problème et veulent une solution très rapide (et sont bloqués sur 9.4.5 ou une version antérieure), voici ce que j'ai fait:

Création de table de test

CREATE TABLE test(id serial, data jsonb);
INSERT INTO test(data) values ('{"name": "my-name", "tags": ["tag1", "tag2"]}');

Mettre à jour l'instruction pour changer le nom de la propriété jsonb

UPDATE test 
SET data = replace(data::TEXT,'"name":','"my-other-name":')::jsonb 
WHERE id = 1;

En fin de compte, la réponse acceptée est correcte en ce sens que vous ne pouvez pas modifier un élément individuel d'un objet jsonb (dans 9.4.5 ou une version antérieure); cependant, vous pouvez convertir l'objet jsonb en une chaîne (:: TEXT), puis manipuler la chaîne et la convertir en l'objet jsonb (:: jsonb).

Il y a deux mises en garde importantes

  1. cela remplacera toutes les propriétés appelées "nom" dans le json (dans le cas où vous avez plusieurs propriétés avec le même nom)
  2. ce n'est pas aussi efficace que jsonb_set le serait si vous utilisez 9.5

Cela dit, je suis tombé sur une situation où je devais mettre à jour le schéma du contenu dans les objets jsonb et c'était le moyen le plus simple d'accomplir exactement ce que l'affiche originale demandait.

Chad Capra
la source
1
Bon seigneur, j'ai cherché comment faire une mise à jour de jsonb pendant environ deux heures afin que je puisse remplacer tous \u0000les caractères nuls, l'exemple a montré l'image complète. Merci pour cela!
Joshua Robinson
3
Cela semble bon! btw le deuxième argument à remplacer dans votre exemple inclut les deux points et le troisième ne le fait pas. Il semble que votre appel devrait êtrereplace(data::TEXT, '"name":', '"my-other-name":')::jsonb
davidicus
Merci @davidicus! Désolé pour la mise à jour très retardée, mais j'apprécie que vous partagiez pour les autres!
Chad Capra
12

Cette question a été posée dans le contexte de postgres 9.4, cependant les nouveaux téléspectateurs venant à cette question doivent savoir que dans postgres 9.5, les opérations de création / mise à jour / suppression du sous-document sur les champs JSONB sont supportées nativement par la base de données, sans avoir besoin d'extension les fonctions.

Voir: JSONB modifiant des opérateurs et des fonctions

bguiz
la source
8

mettre à jour l'attribut 'name':

UPDATE test SET data=data||'{"name":"my-other-name"}' WHERE id = 1;

et si vous vouliez supprimer par exemple les attributs 'name' et 'tags':

UPDATE test SET data=data-'{"name","tags"}'::text[] WHERE id = 1;
Arthur
la source
6

J'ai écrit une petite fonction pour moi-même qui fonctionne de manière récursive dans Postgres 9.4. J'ai eu le même problème (bien, ils ont résolu une partie de ce mal de tête dans Postgres 9.5). Quoi qu'il en soit, voici la fonction (j'espère que cela fonctionne bien pour vous):

CREATE OR REPLACE FUNCTION jsonb_update(val1 JSONB,val2 JSONB)
RETURNS JSONB AS $$
DECLARE
    result JSONB;
    v RECORD;
BEGIN
    IF jsonb_typeof(val2) = 'null'
    THEN 
        RETURN val1;
    END IF;

    result = val1;

    FOR v IN SELECT key, value FROM jsonb_each(val2) LOOP

        IF jsonb_typeof(val2->v.key) = 'object'
            THEN
                result = result || jsonb_build_object(v.key, jsonb_update(val1->v.key, val2->v.key));
            ELSE
                result = result || jsonb_build_object(v.key, v.value);
        END IF;
    END LOOP;

    RETURN result;
END;
$$ LANGUAGE plpgsql;

Voici un exemple d'utilisation:

select jsonb_update('{"a":{"b":{"c":{"d":5,"dd":6},"cc":1}},"aaa":5}'::jsonb, '{"a":{"b":{"c":{"d":15}}},"aa":9}'::jsonb);
                            jsonb_update                             
---------------------------------------------------------------------
 {"a": {"b": {"c": {"d": 15, "dd": 6}, "cc": 1}}, "aa": 9, "aaa": 5}
(1 row)

Comme vous pouvez le voir, analysez en profondeur et mettez à jour / ajoutez des valeurs si nécessaire.

J. Raczkiewicz
la source
Cela ne fonctionne pas dans la version 9.4, car a jsonb_build_objectété introduit dans la version 9.5
Greg
@Greg Vous avez raison, je viens de vérifier et j'utilise PostgreSQL 9.5 maintenant - c'est pourquoi cela fonctionne. Merci de l'avoir signalé - ma solution ne fonctionnera pas dans la version 9.4.
J. Raczkiewicz
4

Peut-être: UPDATE test SET data = '"mon-autre-nom"' :: json WHERE id = 1;

Cela a fonctionné avec mon cas, où les données sont de type json

Gianluigi Sartori
la source
1
A travaillé pour moi aussi, sur postgresql 9.4.5. L'enregistrement entier est réécrit afin que l'on ne puisse pas mettre à jour un seul guichet automatique de champ.
kometen