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();
En fait, C'est tout ce dont vous avez besoin:
NEW := jsonb_populate_record(NEW, NEW.json);
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é :
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é.
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.
Les champs dynamiques sont notoirement difficiles dans PLPGSQL. En particulier, il n'y a aucun moyen que nous puissions écrire new.variable := something
Où variable
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.
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.
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.
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!