Vous n'êtes pas identifié(e).

#1 13/02/2011 23:15:46

bebert73
Membre

curseur avec ORDER BY et UPDATE

Bonjour,

Je suis sous PostgreSQL 8.4

Que signifie exactement le message d'erreur "le curseur ... n'est pas un parcours modifiable..." ?

J'ai bien vu d'où provenait cette erreur, par contre je n'ai pas compris pourquoi.

Soit la table suivante :
CREATE TABLE personnes (
    rang            INTEGER,
    nom        VARCHAR(16)
);
INSERT INTO personnes (rang, nom) VALUES (1, 'toto'), (2, 'titi'), (3, 'tata');


et la fonction suivante qui fait un UPDATE sur la table personnes, à l'aide d'un curseur

CREATE OR REPLACE FUNCTION misajour() RETURNS VOID AS $$
DECLARE  c_personnes SCROLL CURSOR FOR SELECT * FROM personnes ORDER BY rang;
        r_personnes    personnes%ROWTYPE;
BEGIN
    OPEN c_personnes;
    FETCH FIRST FROM c_personnes INTO r_personnes;
    WHILE FOUND = TRUE LOOP
        RAISE NOTICE 'Le nom est %.', r_personnes.nom;
        UPDATE    personnes
        SET     nom = UPPER(nom)
        WHERE    CURRENT OF c_personnes;
       
        FETCH NEXT FROM c_personnes INTO r_personnes;
    END LOOP;
    CLOSE c_personnes;
END;
$$ LANGUAGE PLPGSQL;
SELECT misajour();

psql:test.sql:98: NOTICE:  Le nom est TOTO.
psql:test.sql:98: ERREUR:  le curseur « c_personnes » n'est pas un parcours modifiable de la table « personnes »
CONTEXTE : instruction SQL « UPDATE personnes SET nom = UPPER(nom) WHERE CURRENT OF  $1  »
PL/pgSQL function "misajour" line 10 at instruction SQL


A noter que l'erreur (le curseur « c_personnes » n'est pas un parcours modifiable de la table « personnes ») est clairement due à la combinaison du ORDER BY dans le curseur, et du UPDATE dans le corps de la fonction.

En effet, j'ai fait la même fonction successivement (sans ORDER BY et avec UPDATE), puis (avec ORDER BY et sans UPDATE), et les deux fonctions marchent bien.
Ci-dessous les corps de ces fonctions qui marchent :

-- sans ORDER BY, avec UPDATE
CREATE OR REPLACE FUNCTION misajour() RETURNS VOID AS $$
DECLARE c_personnes SCROLL CURSOR FOR SELECT * FROM personnes;
        r_personnes    personnes%ROWTYPE;
BEGIN
    OPEN c_personnes;
    FETCH FIRST FROM c_personnes INTO r_personnes;
    WHILE FOUND = TRUE LOOP
        RAISE NOTICE 'Le nom est %.', r_personnes.nom;
        UPDATE    personnes
        SET     nom = UPPER(nom)
        WHERE    CURRENT OF c_personnes;
       
        FETCH NEXT FROM c_personnes INTO r_personnes;
    END LOOP;
    CLOSE c_personnes;
END;
$$ LANGUAGE PLPGSQL;
SELECT misajour();



-- avec ORDER BY, sans UPDATE
CREATE OR REPLACE FUNCTION misajour() RETURNS VOID AS $$
DECLARE c_personnes SCROLL CURSOR FOR SELECT * FROM personnes ORDER BY rang;
        r_personnes    personnes%ROWTYPE;
BEGIN
    OPEN c_personnes;
    FETCH FIRST FROM c_personnes INTO r_personnes;
    WHILE FOUND = TRUE LOOP
        RAISE NOTICE 'Le nom est %.', r_personnes.nom;
       
        FETCH NEXT FROM c_personnes INTO r_personnes;
    END LOOP;
    CLOSE c_personnes;
END;
$$ LANGUAGE PLPGSQL;
SELECT misajour();


Pourquoi (avec ORDER BY et avec UPDATE) ça fait cette erreur ?

Dernière modification par bebert73 (13/02/2011 23:26:45)

Hors ligne

#2 14/02/2011 08:45:29

Marc Cousin
Membre

Re : curseur avec ORDER BY et UPDATE

À mon avis (mais je n'ai pas pu le tester), c'est qu'il manque un FOR UPDATE dans la déclaration du curseur:

DECLARE  c_personnes SCROLL CURSOR FOR SELECT * FROM personnes ORDER BY rang FOR UPDATE;

Est-ce que ça marche avec ?


Marc.

Hors ligne

#3 14/02/2011 10:40:31

bebert73
Membre

Re : curseur avec ORDER BY et UPDATE

Bonjour Marc,

en théorie c'est ça, oui...sauf que l'option SCROLL est interdite avec un FOR UPDATE, un curseur déplaçable doit être en READ ONLY !

Il le précisent bien dans la doc, partie 6 chapitre Références, DECLARE, où ils disent clairement que "les parcours inverses sont aussi interdits lorsque la requête inclut les clauses FOR UPDATE et FOR SHARE; donc SCROLL peut ne pas être indiqué dans ce cas"

Mon exemple était mal choisi car je n'ai qu'à enlever le SCROLL, qui ne sert à rien vu que je ne fais pas de parcours arrière.

Mais supposons que je veuille faire un parcourt arrière sur une liste trié par la colonne "rang", comme dans le cas ci-après, pour par exemple mettre en majuscule 1 nom sur 3, mais en partant de la fin :

CREATE TABLE personnes (
    rang    INTEGER,
    nom        VARCHAR(16)
);
INSERT INTO personnes (rang, nom) VALUES    (1, 'toto'), (2, 'titi'), (3, 'tata'), (4, 'tutu'), (5, 'momo'), (6, 'mama'), (7, 'mumu'), (8, 'mimi');

Je veux obtenir ('toto', 'TITI', 'tata', 'tutu', 'MOMO', 'mama', 'mumu', 'MIMI') avec systématiquement le dernier élément de la liste (triée selon rang) en majuscule.

En fait il est impossible de faire ça en faisant un parcours inverse. La solution de contournement est cependant très simple, il suffit de trier la liste par ordre DESC, du coup on peut faire un parcourt avant et on n'a plus besoin du scroll (voir mon exemple 3, ci-dessous).

En fait je me suis un peu embrouillé les pédales car sur le point précis du SCROLL, PostgreSQL ne respecte pas du tout le standard SQL. D'après SQL99, SCROLL est nécessaire pour deux choses :
- pour déplacer le curseur de plus d'une position
- pour déplacer le curseur en sens inverse

Or dans PostgreSQL, on peut déplacer le curseur de plus d'une position, y compris sans utiliser SCROLL (on le voit dans mon exemple 3)
On peut même dans certains cas faire un parcourt inverse sans utiliser SCROLL (extrait de la doc : ...PostgreSQL™  autorise  les  récupérations  remontantes  sans  que  l'option  SCROLL ne  soit  précisé,  sous  réserve  que  le  plan d'exécution du curseur soit suffisamment simple pour être géré sans surcharge. Toutefois, il est fortement conseillé aux développeurs d'application ne pas utiliser les récupérations remontantes avec des curseurs qui n'ont pas été créés avec l'option SCROLL.)


EN RESUME, je retiens qu'en PostgreSQL, on peut très bien ne jamais utiliser l'option SCROLL, car
1°/ elle n'est pas nécessaire pour déplacer le curseur de plusieurs positions,
2°/ un parcourt inverse d'une table peut très bien s'effectuer simplement en triant la table dans le sens DESC







Pour information, ci-dessous les bouts de codes qui illustrent ces différents cas (curseur sans SCROLL, curseur avec SCROLL, et solution de contournement)


-- Exemple 1 : sans le SCROLL, ça produit le message suivant :
ERREUR:  le curseur peut seulement parcourir en avant
ASTUCE : Déclarez-le avec l'option SCROLL pour activer le parcours inverse

CREATE OR REPLACE FUNCTION misajour() RETURNS VOID AS $$
DECLARE c_personnes CURSOR FOR SELECT * FROM personnes ORDER BY rang FOR UPDATE;
        r_personnes    personnes%ROWTYPE;
BEGIN
    OPEN c_personnes;
    FETCH LAST FROM c_personnes INTO r_personnes;
    WHILE FOUND = TRUE LOOP
        RAISE NOTICE 'Le nom est %.', r_personnes.nom;
        UPDATE    personnes
        SET     nom = UPPER(nom)
        WHERE    CURRENT OF c_personnes;
       
        MOVE -2 FROM c_personnes;
        FETCH PRIOR FROM c_personnes INTO r_personnes;
    END LOOP;
        CLOSE c_personnes;
END;
$$ LANGUAGE PLPGSQL;
SELECT misajour();



-- Exemple 2 : avec le SCROLL, ça produit le message suivant
ERREUR:  DECLARE SCROLL CURSOR ... FOR UPDATE/SHARE n'est pas supporté
DÉTAIL : Les curseurs déplaçables doivent être en lecture seule (READ ONLY).

CREATE OR REPLACE FUNCTION misajour() RETURNS VOID AS $$
DECLARE c_personnes SCROLL CURSOR FOR SELECT * FROM personnes ORDER BY rang FOR UPDATE;
        r_personnes    personnes%ROWTYPE;
BEGIN
    OPEN c_personnes;
    FETCH LAST FROM c_personnes INTO r_personnes;
    WHILE FOUND = TRUE LOOP
        RAISE NOTICE 'Le nom est %.', r_personnes.nom;
        UPDATE    personnes
        SET     nom = UPPER(nom)
        WHERE    CURRENT OF c_personnes;
       
        MOVE -2 FROM c_personnes;
        FETCH PRIOR FROM c_personnes INTO r_personnes;
    END LOOP;
        CLOSE c_personnes;
END;
$$ LANGUAGE PLPGSQL;
SELECT misajour();







-- Exemple 3, solution de contournement (ORDER BY rang DESC)

CREATE OR REPLACE FUNCTION misajour() RETURNS VOID AS $$
DECLARE c_personnes CURSOR FOR SELECT * FROM personnes ORDER BY rang DESC FOR UPDATE;
        r_personnes    personnes%ROWTYPE;
BEGIN
    OPEN c_personnes;
    FETCH FIRST FROM c_personnes INTO r_personnes;
    WHILE FOUND = TRUE LOOP
        RAISE NOTICE 'Le nom est %.', r_personnes.nom;
        UPDATE    personnes
        SET     nom = UPPER(nom)
        WHERE    CURRENT OF c_personnes;
       
        MOVE 2 FROM c_personnes;
        FETCH NEXT FROM c_personnes INTO r_personnes;
    END LOOP;
        CLOSE c_personnes;
END;
$$ LANGUAGE PLPGSQL;
SELECT misajour();

Hors ligne

#4 14/02/2011 10:47:12

Marc Cousin
Membre

Re : curseur avec ORDER BY et UPDATE

Oui, j'avais pas vu le mot clé SCROLL. Je devrais arrêter de répondre si tôt le matin smile


Marc.

Hors ligne

#5 14/02/2011 12:17:54

bebert73
Membre

Re : curseur avec ORDER BY et UPDATE

ha ha, oui, ou alors changer de marque de céréales wink

bon, en résumé je retiens que PostgreSQL possède une petite limitation par rapport au standard SQL si on veut parcourir une table dans tous les sens avec un curseur FOR UPDATE (un parcourt en sens purement inverse pouvant se simuler via un ORDER BY DESC)

je viens de tester en ORACLE, là ça marche

ceci étant, ce n'est pas tous les jours qu'on a besoin de faire ce genre de manip...

Hors ligne

#6 14/02/2011 12:23:53

Marc Cousin
Membre

Re : curseur avec ORDER BY et UPDATE

Oui. Ou alors, comme autre contournement, on peut ne pas utiliser le current of, mais passer par la clé primaire. C'est un peu plus lent et moins simple d'usage, mais on s'en sort smile


Marc.

Hors ligne

Pied de page des forums