Supposons que j'ai une table comme suit nommée people
, où id
est une clé primaire:
+-----------+---------+---------+
| id | fname | lname |
| (integer) | (text) | (text) |
+===========+=========+=========+
| 1 | Daniel | Edwards |
| 2 | Fred | Holt |
| 3 | Henry | Smith |
+-----------+---------+---------+
J'essaie d'écrire une requête de duplication de lignes qui est suffisamment robuste pour prendre en compte les modifications de schéma de la table. Chaque fois que j'ajoute une colonne à la table, je ne veux pas avoir à revenir en arrière et modifier la requête de duplication.
Je sais que je peux le faire, ce qui dupliquera l'identifiant d'enregistrement 2 et donnera à l'enregistrement dupliqué un nouvel identifiant:
INSERT INTO people (fname, lname) SELECT fname, lname FROM people WHERE id = 2;
Cependant, si j'ajoute une colonne age
, je devrai modifier la requête pour tenir également compte de la colonne age.
Évidemment, je ne peux pas faire ce qui suit, car cela dupliquera également la clé primaire, résultant en un duplicate key value violates unique constraint
- Et, je ne veux pas qu'ils partagent le même identifiant de toute façon:
INSERT INTO people SELECT * FROM people WHERE id = 2
Cela dit, quelle serait une approche raisonnable pour résoudre ce problème? Je préférerais rester loin des procédures stockées, mais je ne suis pas à 100% contre, je suppose ...
hstore
Si vous avez le module supplémentaire hstore
installé ( instructions dans le lien ci-dessous ), il y a un étonnamment simple façon de remplacer la ou les valeurs de champ (s) individuel (s) sans rien savoir des autres colonnes:
Exemple de base: dupliquez la ligne avec id = 2
Mais remplacez 2
Par 3
:
INSERT INTO people
SELECT (p #= hstore('id', '3')).* FROM people p WHERE id = 2;
Détails:
En supposant (puisque ce n'est pas défini dans la question) que people.id
Est un serial
colonne avec une séquence attachée, vous voudrez la prochaine valeur de la séquence. Nous pouvons déterminer le nom de la séquence avec pg_get_serial_sequence()
. Détails:
Ou vous pouvez simplement coder en dur le nom de la séquence si cela ne change pas.
Nous le ferions avons cette requête:
INSERT INTO people SELECT (p #= hstore('id', nextval(pg_get_serial_sequence('people', 'id'))::text)).* FROM people p WHERE id = 2;
Ce qui fonctionne, mais souffre d'une faiblesse dans le planificateur de requêtes Postgres: l'expression est évaluée séparément pour chaque colonne de la ligne, ce qui gaspille les numéros de séquence et les performances. Pour éviter cela, déplacez l'expression dans une sous-requête et décomposez la ligne une fois uniquement:
INSERT INTO people
SELECT (p1).*
FROM (
SELECT p #= hstore('id', nextval(pg_get_serial_sequence('people', 'id'))::text) AS p1
FROM people p WHERE id = 2
) sub;
Probablement le plus rapide pour une (ou quelques) rangée (s) à la fois.
Si vous n'avez pas hstore
installé et ne pouvez pas installer de modules supplémentaires, vous pouvez faire une astuce similaire avec json_populate_record()
ou jsonb_populate_record()
, mais cette capacité n'est pas documentée et peut ne pas être fiable.
Une autre solution simple serait d'utiliser un transitoire temporaire comme celui-ci:
BEGIN;
CREATE TEMP TABLE people_tmp ON COMMIT DROP AS
SELECT * FROM people WHERE id = 2;
UPDATE people_tmp SET id = nextval(pg_get_serial_sequence('people', 'id'));
INSERT INTO people TABLE people_tmp;
COMMIT;
J'ai ajouté ON COMMIT DROP
Pour supprimer automatiquement le tableau à la fin de la transaction. Par conséquent, j'ai également encapsulé l'opération dans une transaction qui lui est propre. Ni l'un ni l'autre n'est strictement nécessaire.
Cela offre un large éventail d'options supplémentaires - vous pouvez faire n'importe quoi avec la ligne avant l'insertion, mais cela va être un peu plus lent en raison de la surcharge de création et de suppression d'une table temporaire.
Cette solution fonctionne pour une seule ligne ou pour n'importe quel nombre de lignes à la fois. Chaque ligne obtient automatiquement une nouvelle valeur par défaut de la séquence.
Utilisation de la notation courte (standard SQL) TABLE people
.
Pour plusieurs lignes à la fois, le SQL dynamique va être le plus rapide. Concaténez les colonnes de la table système pg_attribute
Ou du schéma d'information et exécutez-la dynamiquement dans une instruction DO
ou écrivez une fonction pour une utilisation répétée:
CREATE OR REPLACE FUNCTION f_row_copy(_tbl regclass, _id int, OUT row_ct int) AS
$func$
BEGIN
EXECUTE (
SELECT format('INSERT INTO %1$s(%2$s) SELECT %2$s FROM %1$s WHERE id = $1',
_tbl, string_agg(quote_ident(attname), ', '))
FROM pg_attribute
WHERE attrelid = _tbl
AND NOT attisdropped -- no dropped (dead) columns
AND attnum > 0 -- no system columns
AND attname <> 'id' -- exclude id column
)
USING _id;
GET DIAGNOSTICS row_ct = ROW_COUNT; -- directly assign OUT parameter
END
$func$ LANGUAGE plpgsql;
Appel:
SELECT f_row_copy('people', 9);
Fonctionne pour n'importe quelle table avec une colonne entière nommée id
. Vous pouvez aussi facilement rendre le nom de la colonne dynamique ...
Ce n'est peut-être pas votre premier choix puisque vous vouliez stay away from stored procedures
, Mais là encore, c'est pas une "procédure stockée" de toute façon ...
En relation:
Une colonne serial
est un cas particulier. Si vous souhaitez remplir plus ou toutes les colonnes avec leurs valeurs par défaut respectives, cela devient plus sophistiqué. Considérez cette réponse connexe:
Essayez de créer un trigger
lors de l'insertion:
CREATE TRIGGER name BEFORE INSERT
Dans ce déclencheur, vous définissez l'ID NULL. Lorsque le déclenchement est terminé, l'insertion est terminée et Postgres fournira un ID. Je suppose que vous avez défini l'ID comme DEFAULT NEXTVAL('A_SEQUENCE'::REGCLASS)
.