Prérequis
Les codes sources utilisés dans cet article utiliseront exclusivement le SGBD PostgreSQL, version 10.6 minimum.
Les champs de clé primaire seront toujours id
.
Le champ servant à stocker l’ordre de tri sera toujours sort
.
L’exemple
L’exemple se basera sur une liste de sportif⋅ve⋅s avec leur ordre d’arrivée à une course. Oui j’invente une course mixte car vive l’égalité.
Voici donc la table qui nous servira dans cette première partie.
CREATE TABLE persons (
id SERIAL PRIMARY KEY,
firstname TEXT NOT NULL CHECK(firstname ~ '[[:alpha:]]+'),
lastname TEXT NOT NULL CHECK(lastname ~ '[[:alpha:]]+'),
sort INTEGER NOT NULL DEFAULT 0
);
Avoir un tri par défaut dès l’insertion
Il est souvent naturel d’entrer dans l’ordre des données. Nous pouvons donc automatiser
ça avec un déclencheur pour avoir une incrémentation du champ sort
lors de l’ajout
d’une donnée en mettant la valeur du champ à MAX(sort) + 1
.
Créons la fonction déjà. Faisons-la générique.
CREATE OR REPLACE FUNCTION put_at_last() RETURNS trigger AS $$
DECLARE
query TEXT;
found INTEGER;
BEGIN
IF TG_OP = 'INSERT' THEN
query := 'SELECT COALESCE(MAX(sort), 0) + 1 FROM ' || quote_ident(TG_TABLE_NAME);
EXECUTE query INTO found;
NEW.sort := found;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Son fonctionnement est simple, lors d’une insertion, pour la table concernée, elle regarde la
valeur maximale dans le champ sort
en veillant, si la table est vide, à fixer cette valeur
à zéro via la fonction COALESCE()
. À cette valeur max trouvée, elle y ajoute 1
, cette
nouvelle valeur est ensuite stokée dans la variable found
, et cette valeur est ensuite
passée au champ sort
de la nouvelle entrée.
Nous avons la fonction, nous avons la table, créons le déclencheur !
CREATE TRIGGER put_at_last
BEFORE INSERT ON persons
FOR EACH ROW EXECUTE PROCEDURE put_at_last();
Voilà, testons vite fait :
INSERT INTO persons (firstname, lastname)
VALUES ('Laetitia', 'Launay'),
('Suzanne', 'Poissonnier'),
('Eugène', 'Payet'),
('Juliette', 'Germain'),
('Sylvie', 'Lemaire'),
('Julien', 'Normand'),
('Antoine', 'Pasquier')
;
En base nous avons maintenant :
id | firstname | lastname | sort
----+-----------+-------------+------
1 | Laetitia | Launay | 1
2 | Suzanne | Poissonnier | 2
3 | Eugène | Payet | 3
4 | Juliette | Germain | 4
5 | Sylvie | Lemaire | 5
6 | Julien | Normand | 6
7 | Antoine | Pasquier | 7
Ainsi, par défaut, nous avons déjà un classement pré-existant.
La requête permettant de connaitre la position relative de chaque personne
Pour changer le classement d’un élément, nous devons connaître l’élément précédent et l’élément suivant, pour changer leur classement également.
Si un élément monte dans le classement, il prendra la position de l’élément précédent, l’élément précédent prendra la position de l’élément déplacé. L’équivalent se produit dans le sens contraire avec l’élément suivant en descendant.
Il est donc nécessaire de connaître la position relative de chaque élément à déplacer, il faut connaître ce qui le précède et ce qui le devance pour adapter les valeurs aux bons éléments.
Reprenons le contenu de notre table exemple. Pour afficher dans l’ordre du classement les éléments, il suffit de la requête basique suivante :
SELECT * FROM persons ORDER BY sort ASC;
L’idéal, serait que pour chaque ligne, il y ait les informations sur l’élément qui
précède et sur celui qui suit. Pour cela nous allons dégainer les fonctions de fenêtrage LAG()
et LEAD()
.
Plaçons notre requête SELECT
dans un CTE et utilisons nos fonctions de fenêtrage
pour connaître sur chaque ligne l’ID précédent, l’ID suivant, la position de
l’élément précédent et le position de l’élément suivant.
Nous obtenons ceci :
WITH cte AS (
SELECT * FROM persons ORDER BY sort ASC
)
SELECT
*,
LAG(id, 1) OVER (ORDER BY sort ASC) AS prev_item,
LEAD(id, 1) OVER (ORDER BY sort ASC) AS next_item,
LAG(sort, 1) OVER (ORDER BY sort ASC) AS prev_sort,
LEAD(sort, 1) OVER (ORDER BY sort ASC) AS next_sort
FROM cte;
Exécutons ce code :
id | firstname | lastname | sort | prev_item | next_item | prev_sort | next_sort
----+-----------+-------------+------+-----------+-----------+-----------+-----------
1 | Laetitia | Launay | 1 | | 2 | | 2
2 | Suzanne | Poissonnier | 2 | 1 | 3 | 1 | 3
3 | Eugène | Payet | 3 | 2 | 4 | 2 | 4
4 | Juliette | Germain | 4 | 3 | 5 | 3 | 5
5 | Sylvie | Lemaire | 5 | 4 | 6 | 4 | 6
6 | Julien | Normand | 6 | 5 | 7 | 5 | 7
7 | Antoine | Pasquier | 7 | 6 | | 6 |
Nous avons ce que nous souhaitons !
Les champs prev_item
et next_item
correspondent respectivement aux ID précédent
et suivant. De même, les champs prev_sort
et next_sort
correspondent aux positions
des éléments précédent et suivant, respectivement.
Ainsi, pour l’ID 1, qui est classé en première position, les champs prev_item
et prev_sort
sont à NULL
, ce qui est normal car il n’y a rien avant. Suivant la même logique,
le dernier élément du classement a les champs next_item
et next_sort
à NULL
.
Tout ceci est trivial pour le moment car le classement correspond à la valeur de la clé primaire vu que c’est l’ordre d’insertion. Dès que les élements vont être triés et retriés, ce ne sera plus le cas, et cette requête nous sera bien utile. Tellement utile qu’il va falloir l’utiliser autrement.
Rendons cette requête générique et réutilisable
Ce qui serait bien, c’est de rendre cette requête générique pour l’utiliser avec toutes les tables présentant la possibilité de trier leur données.
Pour cela il faut créer une fonction prenant le nom de la table en paramètre et
en considérant le fait que la table respecte les conventions que nous nous sommes
fixés (champs id
et sort
), générer la requête qui permet d’obtenir la sortie
vue précédemment.
Un code possible serait le suivant :
CREATE OR REPLACE FUNCTION prev_next_simple_helper (
IN arg_table_name TEXT
)
RETURNS TABLE (
id INTEGER,
sort INTEGER,
prev_item INTEGER,
next_item INTEGER,
prev_sort INTEGER,
next_sort INTEGER
)
AS $$
DECLARE
query TEXT;
BEGIN
query := 'WITH cte AS (
SELECT id, sort
FROM ' || quote_ident(arg_table_name) || '
ORDER BY sort ASC
)
SELECT
*,
LAG (id, 1) OVER (ORDER BY sort ASC) AS prev_item,
LEAD (id, 1) OVER (ORDER BY sort ASC) AS next_item,
LAG (sort, 1) OVER (ORDER BY sort ASC) AS prev_sort,
LEAD (sort, 1) OVER (ORDER BY sort ASC) AS next_sort
FROM cte;
';
RETURN QUERY EXECUTE query;
END; $$
LANGUAGE 'plpgsql';
Petit détail, la requête générée ne comporte que les champs essentiels au classement,
exit donc les champs firstname
et lastname
, seuls les champs id
, sort
et les
champs créés via les fonctions de fenêtrage sont conservés explicitement. Vu que nous voulons
rendre la fonction générique, nous gardons uniquement ce qu’il nous faut pour pouvoir trier.
Voyons si ça fonctionne toujours aussi bien avec notre table exemple :
SELECT * FROM prev_next_simple_helper('persons');
Donne en sortie :
id | sort | prev_item | next_item | prev_sort | next_sort
----+------+-----------+-----------+-----------+-----------
1 | 1 | | 2 | | 2
2 | 2 | 1 | 3 | 1 | 3
3 | 3 | 2 | 4 | 2 | 4
4 | 4 | 3 | 5 | 3 | 5
5 | 5 | 4 | 6 | 4 | 6
6 | 6 | 5 | 7 | 5 | 7
7 | 7 | 6 | | 6 |
Le contenu est identique à ce que nous avions déjà.
Parfait ! Nous avons maintenant une fonction générique permettant de connaître
la position relative de chaque élément d’une table triable respectant la convention
des champs id
et sort
.
Nous allons pouvoir passer au vif du sujet : changer le classement !
Déplacement d’un seul élément
C’est le cas typique où l’interface propose une flèche vers le haut et une flèche vers le bas pour déplacer un élément.
Pour ce cas il faut connaître 3 choses :
- le nom de la table concernée
- la valeur de l’ID de l’élément concerné
- le sens du déplacement (vers le haut, ou vers le bas, croissant, décroissant…)
Utilisons-les dans une fonction. Sa valeur de retour correspondra à la nouvelle position :
CREATE OR REPLACE FUNCTION move_simple(
IN arg_way TEXT,
IN arg_table_name TEXT,
IN arg_item_id INTEGER
)
RETURNS INTEGER AS $$
DECLARE
v_out INTEGER;
BEGIN
-- Code à venir…
RETURN v_out;
END;
$$ LANGUAGE plpgsql;
Commençons par nous entendre sur les valeurs permises pour l’argument arg_way
:
up
et down
me semble appropriées pour notre exemple. Mettons donc un garde-fou en soulevant une erreur si
la valeur de arg_way
ne correspond à aucune des deux valeurs autorisées :
CREATE OR REPLACE FUNCTION move_simple(
IN arg_way TEXT,
IN arg_table_name TEXT,
IN arg_item_id INTEGER
)
RETURNS INTEGER AS $$
DECLARE
v_out INTEGER;
BEGIN
-- Que 'up' et 'down'
IF NOT(arg_way = 'down' OR arg_way = 'up')
THEN
RAISE EXCEPTION 'You must select way as `up` or `down` only.';
END IF;
-- Code à venir…
RETURN v_out;
END;
$$ LANGUAGE plpgsql;
Bien. Il faut également connaître les valeurs minimale et maximale des positions possibles pour
le classement. Pour cela nous déclarons deux nouvelles variables pour les stocker
après les avoir récupérées à l’aide de la fonction prev_next_simple_helper()
que nous avions défini juste avant.
Appelons-les v_min
et v_max
:
CREATE OR REPLACE FUNCTION move_simple(
IN arg_way TEXT,
IN arg_table_name TEXT,
IN arg_item_id INTEGER
)
RETURNS INTEGER AS $$
DECLARE
v_min INTEGER;
v_max INTEGER;
v_out INTEGER;
BEGIN
IF NOT(arg_way = 'down' OR arg_way = 'up')
THEN
RAISE EXCEPTION 'You must select way as `up` or `down` only.';
END IF;
-- On détermine les bornes min et max
SELECT MIN(sort), MAX(sort)
INTO v_min, v_max
FROM prev_next_simple_helper(arg_table_name);
-- Code à venir…
RETURN v_out;
END;
$$ LANGUAGE plpgsql;
Nous commençons à avoir des éléments intéressants pour nous aider à trier.
Maintenant il faut connaître la position actuelle de l’item déplacé. Nous réutilisons
encore prev_next_simple_helper()
pour cela et nous stockons la valeur dans la variable v_sort_curr
.
CREATE OR REPLACE FUNCTION move_simple(
IN arg_way TEXT,
IN arg_table_name TEXT,
IN arg_item_id INTEGER
)
RETURNS INTEGER AS $$
DECLARE
v_min INTEGER;
v_max INTEGER;
v_out INTEGER;
v_sort_curr INTEGER;
BEGIN
IF NOT(arg_way = 'down' OR arg_way = 'up')
THEN
RAISE EXCEPTION 'You must select way as `up` or `down` only.';
END IF;
SELECT MIN(sort), MAX(sort)
INTO v_min, v_max
FROM prev_next_simple_helper(arg_table_name);
-- position actuelle dans le classement
SELECT sort
INTO v_sort_curr
FROM prev_next_simple_helper(arg_table_name)
WHERE id = arg_item_id;
-- Code à venir…
RETURN v_out;
END;
$$ LANGUAGE plpgsql;
Nous connaissons la position actuelle, nous connaissons les bornes… Mettons un autre garde-fou levant une erreur s’il y a tentative de franchissement de ces bornes :
CREATE OR REPLACE FUNCTION move_simple(
IN arg_way TEXT,
IN arg_table_name TEXT,
IN arg_item_id INTEGER
)
RETURNS INTEGER AS $$
DECLARE
v_min INTEGER;
v_max INTEGER;
v_out INTEGER;
v_sort_curr INTEGER;
BEGIN
IF NOT(arg_way = 'down' OR arg_way = 'up')
THEN
RAISE EXCEPTION 'You must select way as `up` or `down` only.';
END IF;
SELECT MIN(sort), MAX(sort)
INTO v_min, v_max
FROM prev_next_simple_helper(arg_table_name);
SELECT sort
INTO v_sort_curr
FROM prev_next_simple_helper(arg_table_name)
WHERE id = arg_item_id;
-- Si on est déjà au plus haut, on ne monte plus pour ne pas
-- se crâmer les ailes comme Icare
-- Si on est déjà au plus bas, on ne creuse pas :-)
IF (v_min = v_sort_curr AND arg_way = 'up')
OR
(v_max = v_sort_curr AND arg_way = 'down')
THEN
RAISE EXCEPTION 'You cannot up or down more this item.';
END IF;
-- Code à venir…
RETURN v_out;
END;
$$ LANGUAGE plpgsql;
Mettons maintenant une valeur par defaut pour la variable v_out
.
CREATE OR REPLACE FUNCTION move_simple(
IN arg_way TEXT,
IN arg_table_name TEXT,
IN arg_item_id INTEGER
)
RETURNS INTEGER AS $$
DECLARE
v_min INTEGER;
v_max INTEGER;
v_out INTEGER DEFAULT 0;
v_sort_curr INTEGER;
BEGIN
IF NOT(arg_way = 'down' OR arg_way = 'up')
THEN
RAISE EXCEPTION 'You must select way as `up` or `down` only.';
END IF;
SELECT MIN(sort), MAX(sort)
INTO v_min, v_max
FROM prev_next_simple_helper(arg_table_name);
SELECT sort
INTO v_sort_curr
FROM prev_next_simple_helper(arg_table_name)
WHERE id = arg_item_id;
IF (v_min = v_sort_curr AND arg_way = 'up')
OR
(v_max = v_sort_curr AND arg_way = 'down')
THEN
RAISE EXCEPTION 'You cannot up or down more this item.';
END IF;
-- Code à venir…
RETURN v_out;
END;
$$ LANGUAGE plpgsql;
À ce stade, notre fonction est exécutable bien que non finalisée. Testons nos garde-fou :
-- Monter d’un cran l’élément ayant l’ID 1 de la table `persons` (sont champs sort est à 1)
SELECT move_simple('up', 'persons', 1);
Donne :
ERROR: You cannot up or down more this item.
CONTEXT: PL/pgSQL function move_simple(text,text,integer) line 30 at RAISE
Bien, cette limite fonctionne. Voyons son opposé :
SELECT move_simple('down', 'persons', 7);
Donne :
ERROR: You cannot up or down more this item.
CONTEXT: PL/pgSQL function move_simple(text,text,integer) line 30 at RAISE
Ce garde-fou fonctionne bien. Voyons s’il ne bloque pas pour les mouvements autorisés :
SELECT move_simple('up', 'persons', 7);
SELECT move_simple('down', 'persons', 1);
SELECT move_simple('down', 'persons', 3);
Nous n’avons aucune erreur en sortie.
Voyons notre garde-fou pour la direction :
SELECT move_simple('foo', 'persons', 3);
Donne :
ERROR: You must select way as `up` or `down` only.
CONTEXT: PL/pgSQL function move_simple(text,text,integer) line 12 at RAISE
Parfait, c’est exactement ce que nous voulions.
Reprenons l’écriture de notre fonction pour y inclure enfin le code permettant de changer notre classement.
Nous avons donc un mouvement possible vers le haut (up
), et vers le bas (down
).
Pour chaque mouvement, nous devons :
- connaitre les infos de l’élément concerné,
- mettre à jour la position de l’élément déplacé,
- mettre à jour la position de l’élément dont il prend la place.
Nous avons donc, pour chaque mouvement, 3 requêtes : un SELECT
et deux UPDATE
.
Pour les SELECT
de chaque mouvement, nous placerons les informations clés dans les nouvelles variables v_sort_other
et v_id_other
.
Les requêtes UPDATE
devant être construites, nous ajoutons encore deux variables : query_curr
et query_other
.
CREATE OR REPLACE FUNCTION move_simple(
IN arg_way TEXT,
IN arg_table_name TEXT,
IN arg_item_id INTEGER
)
RETURNS INTEGER AS $$
DECLARE
v_min INTEGER;
v_max INTEGER;
-- la variable v_out devient inutile maintenant, elle dégage.
v_sort_curr INTEGER;
-- pour le UPDATE de l’élément déplacé
query_curr TEXT;
-- pour le UPDATE de l’élément dont il prend la place
query_other TEXT;
-- Infos utiles pour le mouvement UP et DOWN
v_sort_other INTEGER;
v_id_other INTEGER;
BEGIN
IF NOT(arg_way = 'down' OR arg_way = 'up')
THEN
RAISE EXCEPTION 'You must select way as `up` or `down` only.';
END IF;
SELECT MIN(sort), MAX(sort)
INTO v_min, v_max
FROM prev_next_simple_helper(arg_table_name);
SELECT sort
INTO v_sort_curr
FROM prev_next_simple_helper(arg_table_name)
WHERE id = arg_item_id;
IF (v_min = v_sort_curr AND arg_way = 'up')
OR
(v_max = v_sort_curr AND arg_way = 'down')
THEN
RAISE EXCEPTION 'You cannot up or down more this item.';
END IF;
-- Selon le mouvement, on sélectionne tel ou tel champ
SELECT
CASE WHEN arg_way = 'up' THEN prev_sort ELSE next_sort END,
CASE WHEN arg_way = 'up' THEN prev_item ELSE next_item END
INTO
v_sort_other,
v_id_other
FROM
prev_next_simple_helper(arg_table_name)
WHERE
id = arg_item_id;
-- L’élément déplacé prend la position de l’élément dont il prend la place
query_curr := '
UPDATE '|| quote_ident(arg_table_name) ||'
SET sort = $1
WHERE id = $2;
';
-- l’élément délogé prend la position de l’élément déplacé
query_other := '
UPDATE '|| quote_ident(arg_table_name) ||'
SET sort = $1
WHERE id = $2;
';
EXECUTE query_curr USING v_sort_other, arg_item_id;
EXECUTE query_other USING v_sort_curr, v_id_other;
-- la nouvelle position correspond forcément à la position de l’élément cible
RETURN v_sort_other;
END;
$$ LANGUAGE plpgsql;
Il ne nous reste plus qu’à tester !
-- On déplace Suzanne Poissonnier de la 2e à la 1re place
SELECT move_simple('up', 'persons', 2);
En retour nous avons :
move_simple
-------------
1
(1 row)
Donc la position 1, ce que nous cherchons à obtenir. Est-ce bien le cas dans la table ?
SELECT * FROM persons ORDER BY sort ASC;
Donne en sortie :
id | firstname | lastname | sort
----+-----------+-------------+------
2 | Suzanne | Poissonnier | 1
1 | Laetitia | Launay | 2
3 | Eugène | Payet | 3
4 | Juliette | Germain | 4
5 | Sylvie | Lemaire | 5
6 | Julien | Normand | 6
7 | Antoine | Pasquier | 7
(7 rows)
Remontons Laetitia Launay :
SELECT move_simple('up', 'persons', 1);
Nous avons alors :
id | firstname | lastname | sort
----+-----------+-------------+------
1 | Laetitia | Launay | 1
2 | Suzanne | Poissonnier | 2
3 | Eugène | Payet | 3
4 | Juliette | Germain | 4
5 | Sylvie | Lemaire | 5
6 | Julien | Normand | 6
7 | Antoine | Pasquier | 7
(7 rows)
Descendons Eugène Payet, ayant l’ID 3 :
SELECT move_simple('down', 'persons', 3);
Ce qui donne :
id | firstname | lastname | sort
----+-----------+-------------+------
1 | Laetitia | Launay | 1
2 | Suzanne | Poissonnier | 2
4 | Juliette | Germain | 3
3 | Eugène | Payet | 4
5 | Sylvie | Lemaire | 5
6 | Julien | Normand | 6
7 | Antoine | Pasquier | 7
(7 rows)
Cela semble bien fonctionner n’est-ce pas ? Le classement au coup par coup est fini !
Bien, reste à voir pour le classement en fournissant tous les éléments…
Déplacement de tous les éléments
La réorganisation du classement de tous les éléments se fait plus facilement qu’avec un seul élément à déplacer au coup par coup.
La fonction que nous allons concevoir aura besoin du nom de la table concernée, ainsi que de la liste des ID dans l’ordre voulu. Cette liste sera ici sous la forme d’une chaîne de caractères contenant les ID concaténés avec des virgules. Cette manière de faire est très pratique par exemple avec des scripts Javascript retounant la liste des ID dans un nouvel ordre après des glisser-déposer à la souris. Vous êtes libre d’adapter avec par exemple un JSON ou autre.
Par ailleurs cette fonction ne retournera rien.
Décortiquons un peu cette fonction par étape. Commençons par la signature :
CREATE OR REPLACE FUNCTION sort_simple(
IN arg_table_name TEXT,
IN arg_item_ids TEXT
)
RETURNS VOID AS $$
DECLARE
ids INTEGER[];
i INTEGER;
id INTEGER;
q TEXT;
BEGIN
-- Code à venir…
END;
$$ LANGUAGE plpgsql;
Les deux arguments de la fonction parlent d’eux-même.
Les variables déclarées au sein de la fonction sont les suivantes :
ids
va contenir les ID provenant dearg_item_ids
sous la forme d’un tableau d’entiersi
va servir à remplir le champsort
de la table visée de façon incrémentale,id
sera la valeur de l’ID d’un item dans la boucle,q
sera la chaîne de caractères contenant la requête s’exécutant à chaque tour de la boucleFOREACH
pour mettre à jour le classement.
Commençons par récupérer les IDs sous la forme d’un tableau d’entiers, en indiquant
que la chaîne de caractères doit être scindée selon le caractère séparateur ,
,
et en convertissant dans la foulée ses éléments en entiers.
CREATE OR REPLACE FUNCTION sort_simple(
IN arg_table_name TEXT,
IN arg_item_ids TEXT
)
RETURNS VOID AS $$
DECLARE
ids INTEGER[];
i INTEGER;
id INTEGER;
q TEXT;
BEGIN
-- Convertissons en un tableau de nombres entiers
ids := string_to_array(arg_item_ids, ',')::integer[];
-- Code à venir…
END;
$$ LANGUAGE plpgsql;
Initialisons l’incrémentation et déclarons la requête de mise à jour.
CREATE OR REPLACE FUNCTION sort_simple(
IN arg_table_name TEXT,
IN arg_item_ids TEXT
)
RETURNS VOID AS $$
DECLARE
ids INTEGER[];
i INTEGER;
id INTEGER;
q TEXT;
BEGIN
ids := string_to_array(arg_item_ids, ',')::integer[];
-- Initialisation de l’incrémentation, zéro par défaut
i := 0;
-- La requête d’UPDATE
q := 'UPDATE ' || quote_ident(arg_table_name) || ' SET sort=$1 WHERE id=$2';
-- Code à venir…
END;
$$ LANGUAGE plpgsql;
Il ne reste plus qu’à créer la boucle sur le tableau d’IDs, incrémenter à chaque tour, et le tour est joué :
CREATE OR REPLACE FUNCTION sort_simple(
IN arg_table_name TEXT,
IN arg_item_ids TEXT
)
RETURNS VOID AS $$
DECLARE
ids INTEGER[];
i INTEGER;
id INTEGER;
q TEXT;
BEGIN
ids := string_to_array(arg_item_ids, ',')::integer[];
i := 0;
q := 'UPDATE ' || quote_ident(arg_table_name) || ' SET sort=$1 WHERE id=$2';
-- On boucle sur les IDS…
FOREACH id IN ARRAY ids
LOOP
-- On incrémente la position
i := i + 1;
-- On met à jour la position pour l’ID donné
EXECUTE q USING i, id;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Et si nous testions cela maintenant ? On va reprendre notre table telle qu’elle était au début.
SELECT sort_simple('persons', '7,1,6,2,5,3,4');
Regardons le contenu de la table :
id | firstname | lastname | sort
----+-----------+-------------+------
7 | Antoine | Pasquier | 1
1 | Laetitia | Launay | 2
6 | Julien | Normand | 3
2 | Suzanne | Poissonnier | 4
5 | Sylvie | Lemaire | 5
3 | Eugène | Payet | 6
4 | Juliette | Germain | 7
(7 rows)
Ça fonctionne parfaitement ! Parfaitement ? Et si les IDs fournis ne correspondent pas au nombre d’éléments de la table, il se passe quoi ? De même si le nombre est bon, mais qu’il y a des doublons ?
Il nous faut donc ajouter quelques sécurités supplémentaires :
- tester si le nombre d’ID correspond au nombre d’éléments dans la table,
- tester s’il y a des doublons.
Si un des tests échoue, la fonction devra lever une erreur et ne rien faire d’autre.
Commençons par ajouter la sécurité sur le nombre d’éléments. Nous allons récupérer
le nombre d’éléments de la table via un SELECT
et le comparer avec le nombre d’éléments
dans le tableau des IDs.
CREATE OR REPLACE FUNCTION sort_simple(
IN arg_table_name TEXT,
IN arg_item_ids TEXT
)
RETURNS VOID AS $$
DECLARE
ids INTEGER[];
i INTEGER;
id INTEGER;
nt INTEGER;
qn TEXT;
q TEXT;
BEGIN
ids := string_to_array(arg_item_ids, ',')::integer[];
i := 0;
-- requête pour connaître le nombre d’éléments dans la table
qn := 'SELECT COUNT(*) FROM ' || quote_ident(arg_table_name);
q := 'UPDATE ' || quote_ident(arg_table_name) || ' SET sort=$1 WHERE id=$2';
EXECUTE qn INTO nt;
IF nt <> cardinality(ids)
THEN
RAISE EXCEPTION 'IDS amount must be the same as amount of elements in the table.';
END IF;
FOREACH id IN ARRAY ids
LOOP
i := i + 1;
EXECUTE q USING i, id;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Testons avec un nombre d’IDs incorrect :
SELECT sort_simple('persons', '1,7');
La sortie contient alors :
ERROR: IDS amount must be the same as amount of elements in the table.
CONTEXT: PL/pgSQL function sort_simple(text,text) line 20 at RAISE
Et maintenant avec un nombre correct d’IDs :
SELECT sort_simple('persons', '7,6,5,4,3,2,1');
La sortie ne présente alors pas d’erreur. Voyons le contenu de la table :
id | firstname | lastname | sort
----+-----------+-------------+------
7 | Antoine | Pasquier | 1
6 | Julien | Normand | 2
5 | Sylvie | Lemaire | 3
4 | Juliette | Germain | 4
3 | Eugène | Payet | 5
2 | Suzanne | Poissonnier | 6
1 | Laetitia | Launay | 7
(7 rows)
Parfait. Occupons-nous maintenant de tester la présence de doublons.
Pour cela, nous allons utiliser la fonction UNNEST()
sur le tableau des IDs et
regrouper les IDs identiques et les compter. Si le compte est différent de zéro,
c’est que nous avons des doublons.
CREATE OR REPLACE FUNCTION sort_simple(
IN arg_table_name TEXT,
IN arg_item_ids TEXT
)
RETURNS VOID AS $$
DECLARE
ids INTEGER[];
i INTEGER;
id INTEGER;
nt INTEGER;
d BOOLEAN;
qn TEXT;
q TEXT;
BEGIN
ids := string_to_array(arg_item_ids, ',')::integer[];
i := 0;
qn := 'SELECT COUNT(*) FROM ' || quote_ident(arg_table_name);
q := 'UPDATE ' || quote_ident(arg_table_name) || ' SET sort=$1 WHERE id=$2';
EXECUTE qn INTO nt;
IF nt <> cardinality(ids)
THEN
RAISE EXCEPTION 'IDS amount must be the same as amount of elements in the table.';
END IF;
-- Vérifions s’il y a des doublons avec un petit CTE des familles
WITH dq AS (
SELECT item
FROM unnest(ids) AS item
GROUP BY item
HAVING count(*) > 1
)
SELECT COUNT(*) > 0
INTO d
FROM dq;
-- Alors docteur, doublon ou pas doublon ?
IF d
THEN
RAISE EXCEPTION 'IDS must be unique!';
END IF;
FOREACH id IN ARRAY ids
LOOP
i := i + 1;
EXECUTE q USING i, id;
END LOOP;
END;
$$ LANGUAGE plpgsql;
Testons notre nouveau garde-fou :
SELECT sort_simple('persons', '1,2,2,4,5,6,7');
Et la bonne erreur est levée :
ERROR: IDS must be unique!
CONTEXT: PL/pgSQL function sort_simple(text,text) line 37 at RAISE
Testons avec une liste sans doublons :
SELECT sort_simple('persons', '1,2,3,4,5,6,7');
Pas d’erreur… Voyons si ça fonctionne toujours en regardant le contenu de la table :
id | firstname | lastname | sort
----+-----------+-------------+------
1 | Laetitia | Launay | 1
2 | Suzanne | Poissonnier | 2
3 | Eugène | Payet | 3
4 | Juliette | Germain | 4
5 | Sylvie | Lemaire | 5
6 | Julien | Normand | 6
7 | Antoine | Pasquier | 7
(7 rows)
N’est-ce pas génial non ?
Conclusion
Vous avez maintenant ce qu’il faut pour trier le contenu d’une table. Dans la deuxième partie de cette série d’articles, je vous montrerai comment faire la même chose pour des contenus de table dépendant d’un conteneur. Par exemple, des élements appartenant à un groupe, comment trier ces élements de façon générique au sein du groupe.
Pour résumer, quelques exemples commentés :
-- Remonte d’un cran dans le classement l’élément de la table top10 ayant l’ID 3
SELECT move_simple('up', 'top10', 3);
-- Abaisse de deux crans dans le classement l’élément de la table top10 ayant l’ID 1
SELECT move_simple('down', 'top10', 1);
SELECT move_simple('down', 'top10', 1);
-- Réordonne l’ensemble des éléments de la table `top10` en fournissant les ID
-- dans l’ordre désiré.
-- On a donc :
-- ID 1 en première position,
-- ID 5 en deuxième position,
-- ID 10 en troisième position, etc
SELECT sort_simple('top10', '1,5,10,3,4,7,8,9,6,2');
Voilà, amusez-vous avec ces codes, adaptez-les, améliorez-les !
Mise à jour : la deuxième partie est maintenant disponible : Trier avec PostgreSQL, partie 2.