web-dev-qa-db-fra.com

Utilisez la gâchette pour synchroniser des colonnes avec des champs dans la colonne JSON sur insertion ou mise à jour

Je suis un peu d'un débutant de base de données/postgres, alors portez-moi avec moi.
[.____] Si j'ai une table, quelque chose comme ça.

CREATE TABLE testy (
    id INTEGER REFERENCES other_table,
    name varchar(128) PRIMARY KEY,
    json JSONB NOT NULL
);

Je cherche à créer une gâchette avant l'insertion ou la mise à jour qui définira les colonnes id et name sur les valeurs des champs avec les mêmes noms de json.

Donc, par exemple, si testy contenait le ci-dessous et UPDATE testy SET json = '{"id":2,"name":"jim"}' WHERE id = 1 a été appelé.

id | name | json
---+------+-----
 1 | "jim"| {"id":1,"name":"jim"}

Le résultat souhaité serait

id | name | json
---+------+-----
 2 | "jim"| {"id":2,"name":"jim"}

Je souhaite rendre cela assez générique afin que les noms de colonne n'ont pas besoin d'être codés durs. Réglage de la colonne sur NULL si le champ JSON correspondant n'existe pas, c'est bien. Jusqu'à présent j'ai

CREATE TABLE testy_index (
    id INTEGER PRIMARY KEY
);

INSERT INTO testy_index VALUES (1);
INSERT INTO testy_index VALUES (2);
INSERT INTO testy_index VALUES (3);

CREATE TABLE testy (
    id INTEGER REFERENCES testy_index,
    json JSONB NOT NULL
);

CREATE UNIQUE INDEX testy_id ON testy((json->>'id'));

CREATE OR REPLACE FUNCTION json_fn() RETURNS TRIGGER AS $testy$
    DECLARE
        roow RECORD;
    BEGIN
        FOR roow IN 
            SELECT column_name FROM information_schema.columns WHERE table_name = 'testy'
        LOOP
            NEW.roow.column_name = (NEW.json->>roow.column_name);
        END LOOP;
    END;
$testy$ LANGUAGE plpgsql;

CREATE TRIGGER json_trigger
BEFORE INSERT OR UPDATE ON testy FOR EACH ROW
EXECUTE PROCEDURE json_fn();

Ce qui ne fonctionne pas comme vous ne pouvez pas utiliser ROOW.COLUMN_NAME qui flexible. J'ai essayé de jouer avec exécuter sans succès, bien que ce soit possible, je ne le fais tout simplement pas correctement.

Toute aide serait grandement appréciée !!

EDIT: La motivation pour cela est de sorte que les contraintes de clé étrangère puissent être placées sur quelque chose qui se comporte comme champ JSON.

EDIT: PLV8 est génial. Utilisé une version modifiée de la réponse de @Daniel Vérité afin que les colonnes ne soient pas représentées comme des champs de JSON soient annulées

CREATE OR REPLACE FUNCTION json_fn() RETURNS trigger AS
$$
  var obj = JSON.parse(NEW.json);
  for(var col in NEW){
      if(col == 'json'){
        continue;
      }
      if(col in obj){
        NEW[col]=obj[col];
      }else{
        NEW[col]=null;
      }
  }
  return NEW;
$$
LANGUAGE plv8;

CREATE TRIGGER json_trigger
BEFORE INSERT OR UPDATE ON testy FOR EACH ROW
EXECUTE PROCEDURE json_fn();
4
kag0

En fait, C'est tout ce dont vous avez besoin:

NEW := jsonb_populate_record(NEW, NEW.json);

par documentation :

jsonb_populate_record(base anyelement, from_json jsonb)

Élargit l'objet dans from_json à une rangée dont les colonnes correspondent au type d'enregistrement défini par la base (voir la note ci-dessous).

Qu'est-ce que c'est non documenté : la ligne fournie comme premier argument conserve toutes les valeurs non écrasées (aucune clé de correspondance de la valeur JSON). Je ne vois aucune raison pour laquelle cela devrait changer, mais vous ne pouvez pas compter pleinement à ce sujet à moins que ce soit documenté.

Une chose à noter - vous avez écrit:

Réglage de la colonne sur NULL si le champ JSON correspondant n'existe pas, c'est bien.

Ceci retient toutes les valeurs sans clé de correspondance de la valeur JSON, qui devrait être encore meilleure.

Si "non documenté" est trop incertain pour vous, utilisez l'opérateur hstore(#= Faire exactement la même .

NEW := (NEW #= hstore(jsonb_populate_record(NEW, NEW.json)));

Le module hstore doit être installé dans la plupart des systèmes de toute façon. Instructions:

Les deux solution peuvent être dérivées de ma réponse qui Daniel déjà référencé :

Code de fonctionnement

CREATE OR REPLACE FUNCTION json_fn()
  RETURNS TRIGGER AS
$func$
BEGIN
   NEW := jsonb_populate_record(NEW, NEW.json); -- or hstore alternative
   RETURN NEW;
END
$func$ LANGUAGE plpgsql;

Tout le reste de votre configuration a l'air bien, il suffit d'ajouter un PK à testy:

CREATE TABLE testy (
    id   int   PRIMARY KEY REFERENCES testy_index
  , data jsonb NOT NULL
);

Testé dans PG 9.4 et cela fonctionne pour moi comme annoncé. Je doute que la fonction Plv8 puisse rivaliser de la performance et de la simplicité.

Définir d'autres colonnes sur NULL

Selon le commentaire:

CREATE OR REPLACE FUNCTION json_fn()
  RETURNS TRIGGER AS
$func$
DECLARE
  _j jsonb := NEW.json;  -- remember the json value
BEGIN
   NEW := jsonb_populate_record(NULL::testy, _j);
   NEW.json := _j;   -- reassign
   RETURN NEW;
END
$func$ LANGUAGE plpgsql;

Évidemment, vous devez vous assurer que le nom de la colonne ou votre colonne jsonb n'apparaît pas comme nom de clé de la valeur JSON. Et je n'utiliserais pas json comme nom de colonne, car il s'agit d'un nom de type de données et qui peut être déroutant.

2
Erwin Brandstetter

Les champs dynamiques sont notoirement difficiles dans PLPGSQL. En particulier, il n'y a aucun moyen que nous puissions écrire new.variable := somethingvariable signifie un nom de colonne.

Voir Comment définir la valeur du champ variable composite à l'aide de Dynamic SQL pour des moyens qui impliquent interroger le catalogue au moment de l'exécution.

Personnellement, je suggérerais une solution plus simple avec le plv8 Langue.

CREATE FUNCTION json_fn() RETURNS trigger AS
$$
  var obj = JSON.parse(NEW.json);
  for (var key in obj) {
    NEW[key]=obj[key];
  }
  return NEW;
$$
LANGUAGE plv8;

CREATE TRIGGER json_trigger
BEFORE INSERT OR UPDATE ON testy FOR EACH ROW
EXECUTE PROCEDURE json_fn();

Sinon, vous pouvez envisager si votre implémentation n'est pas simplifiée en déplaçant le champ json hors de ce tableau et dans une table séparée, en supposant qu'il soit toujours compatible avec votre intention générale.

3
Daniel Vérité

Je mets un peu de temps pour essayer de développer une réponse pour cette question qui peut correspondre à vos besoins, mais que je n'ai pas de critères détaillés, cela peut ne pas être parfait. Espérons que, cependant, il est assez proche de manière à pouvoir manipuler pour répondre à vos besoins de conception.

Hypothèses initiales

Pour commencer, je devais faire quelques hypothèses initiales pour concevoir l'algorithme.

1) Lorsque vous utilisez cette fonction, vous avez accès au nom de la table sur laquelle vous effectuez la mise à jour simultanée JSON et votre mise à jour de la colonne "Clé étrangère". Il doit être une variable (comme je l'ai fait) ou qu'il doit être codé dur dans chaque instance de la fonction séparément.

2) Basé sur votre requête UPDATE spécifiée à l'origine, j'ai observé que vous l'aviez conçue comme UPDATE testy SET json = '{"id":2,"name":"jim"}' WHERE id = 1, vous indiquant que vous avez ailleurs dans votre application a trouvé un moyen d'obtenir votre prédicat. condition, WHERE id = 1. Cela servira donc pas d'entrée à la fonction.

) Si cela est destiné à être utilisé comme une seule fonction pouvant être appliquée sur de nombreuses tables, améliorez sa réévaluation, vous devez alors que le nom de la colonne JSON pertinent soit identique sur chaque tableau. Sinon, vous devez faire un travail supplémentaire pour examiner le type de données des colonnes et quelques autres cas que je peux penser où cela pourrait se tromper. Nommez simplement toutes ces colonnes comme json_field et tout ira bien.

Sans autre ADO, sur la fonction.

Définition de la fonction

CREATE OR REPLACE FUNCTION json_prop(json_entry json, table_update text, where_clause text)  
RETURNS void AS                                 
$func$
DECLARE
   sql text := 'UPDATE ';
   colname_row RECORD;
BEGIN

  sql := sql || table_update || ' SET ';

  FOR colname_row IN 
  (SELECT col_tab.column_name FROM 
  (SELECT column_name FROM information_schema.columns WHERE table_name = table_update)
  AS col_tab 
  WHERE (json_entry->column_name) IS NOT NULL) 
  LOOP
    sql := sql || colname_row.column_name::text || ' = ''' || ((json_entry)->>(colname_row.column_name::text)) || ''', ';
  END LOOP;

  sql := sql || 'json_field = ''' || json_entry || ''' ';
  sql := sql || where_clause || ';';

   EXECUTE sql;
END
$func$ LANGUAGE plpgsql;

Donc, en bref, la fonction prend trois arguments d'entrée et les utilise pour générer une instruction SQL dynamique, qu'elle exécute ensuite.

entrées

json_entry - Exactement ce que vous pensez que c'est. L'entrée JSON que vous souhaitez mettre à jour.

table_update - La table cible que vous souhaitez mettre à jour.

where_clause - Comme je l'ai mentionné ci-dessus, depuis que je faisais l'hypothèse sur la base de votre description que vous aviez pré-établi des prédicats, cette entrée va ici.

Opération

La fonction recherche des colonnes dans la table table_update, recherchant des colonnes dont les noms correspondent aux clés de noms dans le champ json_entry, en effectuant la sous-sélection SELECT column_name FROM information_schema.columns WHERE table_name = table_update.

Pour toute clé JSON qui correspond à un nom de colonne, le nom de la colonne est renvoyé par la sélection externe SELECT col_tab.column_name FROM ... AS col_tab WHERE (json_entry->column_name) IS NOT NULL).

La boucle FOR est iTerate sur chacun de ces noms de colonne correspondants, ajoutant à l'instruction SQL dynamique les informations nécessaires pour mettre à jour les données de colonne correspondantes.

( [~ # ~] Remarque [~ # ~ ~] Les champs "étrangers" ont été ignorés. C'est-à-dire que s'il existe une clé JSON pour laquelle il n'y a pas de nom de colonne correspondant, ou il existe un nom de colonne qui n'est pas dans l'entrée json_entry, ces champs sont ignorés.

appeler la fonction

La fonction peut être invoquée en appelant simplement

SELECT * FROM json_prop('{"id":2,"name":"james"}'::json, 'testy', 'WHERE id = 1');

modifications possibles

Encore une fois, cela peut ne pas être parfait pour vos besoins. Vous avez mentionné que vous envisagez de l'utiliser comme une gâchette, alors vous auriez alors besoin de RETURN un déclencheur à la place et de configurer vos déclencheurs. Vous n'aimerez peut-être pas que j'ai ignoré des champs "étrangers", et vous voudrez peut-être faire des colonnes NULL ou signaler une erreur dans ces cas? Peut-être que j'ai fait la mauvaise hypothèse sur l'accès aux prédicats?

Cela n'est certainement pas une mise en œuvre fonctionnelle parfaite, mais cela vous suffira probablement de le modifier pour répondre à vos besoins.

Bonne chance!

2
Chris