PostgreSQL
La base de données la plus sophistiquée au monde.

Ouverture de session

Navigation

Contactez-nous

Administration du site :
"equipe chez postgresqlfr point org"

Contact presse :
"fr chez postgresql point org"

Contact association :
"bureau chez postgresqlfr point org"

Questions PostgreSQL :
 IRC :
  serveur irc.freenode.net
  canal #postgresqlfr

Recherche

Accéder aux archives

« Octobre 2008  
Lun Mar Mer Jeu Ven Sam Dim
  2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31  

Syndication

Flux XML

Sondage

Quelle est la version de PostgreSQL la plus répandue sur vos serveurs ?
8.3
10%
8.2
42%
8.1
40%
8.0
2%
7.4
6%
7.3 ou antérieure
0%
Nombre de votes: 48

Écrire et utiliser des fonctions retournant une valeur de type composite (ROWTYPE)

Technique | Écrire et utiliser des fonctions retournant une valeur de type composite (ROWTYPE)

Par RockyRoad le 27/08/2008 - 19:42

Bien que nouvelle utilisatrice de PG, j'ai choisi de présenter un mini-article sur l'utilisation des ROWTYPE dans PL/pgSQL, car je crois qu'il pourrait être utile à beaucoup.

Je n'ai hélas pas beaucoup de temps à y consacrer, mais je compte sur votre participation pour m'aider à le clarifier.

En effet je n'ai rien trouvé de tel sur le web et j'ai dû passer du temps, pour "trouver le pot aux roses", c'est-à-dire une syntaxe correcte utilisable pour mon application (GPL) que je vous dévoilerai plus tard.

Merci à Guillaume Lelarge pour son aide. Au-delà de partager son expérience, il s'est joint activement à mes réflexions et suggéré les solutions qui m'ont mise sur la bonne voie.

Lorsqu'on crée une table, PG crée automatiquement un type (structure) décrivant la composition d'une ligne, c'est-à-dire la liste des champs. Si on veut manipuler des lignes indépendamment d'une table, on peut déclarer un type, par exemple:

test=>
DROP TYPE IF EXISTS names CASCADE;
CREATE TYPE names AS (
first_name varchar,
last_name varchar,
age integer
);

L'interpréteur psql nous répond:

CREATE TYPE

La fonction suivante construit une ligne du type 'names' :

test=>
CREATE OR REPLACE FUNCTION BuildName(_first varchar, _last varchar) RETURNS names AS $$
DECLARE
result names;
BEGIN
RAISE NOTICE 'BuildName(''%'', ''%'')', _first, _last;
result.first_name = _first;
result.last_name = _last;
return result;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
test=>
SELECT BuildName('Pierre', 'Dupond');

On reçoit le message, puis le résultat en une seule colonne

NOTICE:  BuildName('Pierre', 'Dupond')
buildname
------------------
(Pierre,Dupond,)
(1 row)

Si on veut distinguer les colonnes, conformément à la définition du type, il faut utiliser la syntaxe ().*

test=>
SELECT (BuildName('Pierre', 'Dupond')).*;
NOTICE:  BuildName('Pierre', 'Dupond')
NOTICE: BuildName('Pierre', 'Dupond')
NOTICE: BuildName('Pierre', 'Dupond')
first_name | last_name | age
------------+-----------+-----
Pierre | Dupond |
(1 row)

On obtient bien le résultat en 3 colonnes. Mais on reçoit 3 FOIS le message (il y a 3 champs).

Que se passe-t-il ?

Le comportement est identique avec une table:

test=>
DROP TABLE IF EXISTS person CASCADE;
CREATE TABLE person (
id integer PRIMARY KEY,
name varchar,
data integer
);
NOTICE:  table "person" does not exist, skipping
DROP TABLE
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "person_pkey" for table "person"
CREATE TABLE
test=>
CREATE OR REPLACE FUNCTION BuildPerson(_num integer, _name varchar) RETURNS person AS $$
DECLARE
result person;
BEGIN
RAISE NOTICE 'BuildPerson(%, ''%'')', _num, _name;
result.id = _num;
result.name = _name;
return result;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
test=>
SELECT BuildPerson(1, 'Dupond');

On reçoit le message, puis le résultat en une seule colonne

NOTICE:  BuildPerson(1, 'Dupond')
buildperson
-------------
(1,Dupond,)
(1 row)

Si on veut insérer les résultats dans la table, il faut adapter la syntaxe pour obtenir des champs distincts

test=>
SELECT (BuildPerson(2, 'Dupond')).*;
NOTICE:  BuildPerson(2, 'Dupond')
NOTICE: BuildPerson(2, 'Dupond')
NOTICE: BuildPerson(2, 'Dupond')
id | name | data
----+--------+------
2 | Dupond |
(1 row)

test=>
INSERT INTO person (SELECT (BuildPerson(2, 'Dupond')).*);
NOTICE:  BuildPerson(2, 'Dupond')
NOTICE: BuildPerson(2, 'Dupond')
NOTICE: BuildPerson(2, 'Dupond')
INSERT 0 1

On reçoit aussi 3 FOIS le message , mais la rangée n'est insérée qu'une fois

test=>
SELECT * FROM person;
 id |  name  | data
----+--------+------
2 | Dupond |
(1 row)

Tout va bien ? On avance ... mais:

Si la fonction comporte elle-même une instruction INSERT, on peut avoir un problème:

test=>
CREATE OR REPLACE FUNCTION AddPerson(_num integer, _name varchar) RETURNS person AS $$
DECLARE
result person;
BEGIN
RAISE NOTICE 'AddPerson(%, ''%'')', _num, _name;
result.id = _num;
result.name = _name;
INSERT INTO person SELECT result.*;
return result;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
test=>
SELECT (AddPerson(3, 'Durand')).*;
NOTICE:  AddPerson(3, 'Durand')
NOTICE: AddPerson(3, 'Durand')
ERROR: duplicate key value violates unique constraint "person_pkey"

Eh oui ! Comme les messages répétés pouvaient nous le laisser prévoir, la fonction est appelée plusieurs fois, avec les mêmes arguments, ce que notre clé primaire interdit.

Remarquez que, la transaction ayant échoué, aucune ligne n'est finalement ajoutée.

test=>
SELECT * FROM person;
 id |  name  | data
----+--------+------
2 | Dupond |
(1 row)

Pas de souci cependant si on Ă©vite la syntaxe ().* lors de l'invocation:

test=>
DELETE FROM person WHERE id=3;
SELECT AddPerson(3, 'Durand');
SELECT * FROM person;
DELETE 0
NOTICE: AddPerson(3, 'Durand')
addperson
-------------
(3,Durand,)
(1 row)
 
id | name | data
----+--------+------
2 | Dupond |
3 | Durand |
(2 rows)

Attention si on récupère le résultat dans une variable (par exemple dans une fonction récursive)

test=>
CREATE OR REPLACE FUNCTION DoAddPerson(_num integer, _name varchar) RETURNS person AS $$
DECLARE
result person;
BEGIN
RAISE NOTICE 'DoAddPerson(%, ''%'')', _num, _name;
SELECT * INTO result FROM AddPerson(_num, _name);
-- result := AddPerson(_num, _name); -- syntaxe équivalente à la ligne précédente
return result;
END;
$$ LANGUAGE plpgsql;
CREATE FUNCTION
test=>
SELECT DoAddPerson(4, 'Dubois');
SELECT * FROM person;
NOTICE:  DoAddPerson(4, 'Dubois')
NOTICE: AddPerson(4, 'Dubois')
doaddperson
-------------
(4,Dubois,)
(1 row)
 
id | name | data
----+--------+------
2 | Dupond |
3 | Durand |
4 | Dubois |
(3 rows)

Conclusion

L'opérateur .* équivaut à invoquer la source autant de fois que celle-ci a de champs. Si la source est une fonction, cette fonction est donc appelée plusieurs fois (s'il y a plusieurs champs ;)

C'est-Ă -dire que lorsqu'on Ă©crit:

test=>
INSERT INTO person (SELECT (BuildPerson(2, 'Dupond')).*);

tout se passe comme si on avait Ă©crit:

test=>
INSERT INTO person (SELECT (BuildPerson(2, 'Dupond')).id,
(BuildPerson(2, 'Dupond')).name,
(BuildPerson(2, 'Dupond')).data);
NOTICE:  BuildPerson(2, 'Dupond')
NOTICE: BuildPerson(2, 'Dupond')
NOTICE: BuildPerson(2, 'Dupond')
INSERT 0 1

N'hésitez pas à me faire part de vos remarques, suggestions, et bien sûr expérimentations autour de cet article :)

— Michelle Baert —

© PostgreSQLFr, tous droits rĂ©servĂ©s.
Site déclaré à la CNIL sous le numéro 1074678, conformément à la Loi en vigueur.