Accueil

Traduction

Débuter en programmation web - sommaire

Débuter en programmation web - recherche

L'auteur : Patrick Darcheville

Vous pouvez me contacter via Facebook pour questions & suggestions : Page Facebook relative à mon site

Les requêtes sélection

Dans le chapitre précédent je vous ai communiqué le code SQL de plusieurs requêtes SELECT mais sans donner d'explications sur cette syntaxe.
Tout d'abord une définition : une requête sélection (ou requête SELECT) extrait des données d'une ou plusieurs tables de la base. Une requête sélection ne modifie jamais les données de la base.

Afficher une date de façon lisible pour un francophone

Dans le chapitre précédent et en cliquant sur le lien "afficher les infos" du menu, les dates de création de ces news apparaissaient sous le format anglophone : AAAAMMJJ.
La requête source était en effet :
SELECT * FROM news ORDER BY id DESC
Le symbole * signifie "tous les champs" de la table.
"ORDER BY id DESC" signifie que les enregistrements sont ordonnées par ID décroissants : du plus grand au plus petit.

Nouvelle interface

Que vous cliquiez sur le premier lien ou sur le deuxième le rendu est le même.
Les news apparaissent par ordre décroissant d'ID mais les dates sont au format JJMMAAAA.
Cependant concernant le code SQL j'ai utilisé deux techniques différentes pour afficher les dates au format francophone.

Remarquez la deuxième new : tentative malveillante d'insertion de code HTML dans la table 'news'. Mais heureusement l'instruction HTML a été convertie en chaine.
Je n'en dis pas plus pour le moment ; les failles de sécurité et leurs parades seront évoquées dans les chapitres suivants.

Comment afficher les dates au format francophone : JJMMAAAA ?

Première solution

La première solution consiste à utiliser les fonctions SQL day(), month(), year() pour créer des champs calculés.

Le code de la requête SQL devient alors :

SELECT DAY(date_new) AS jour, MONTH(date_new) AS mois, YEAR(date_new) AS annee, 
		titre, message FROM news ORDER BY id DESC  ;

Notez bien la syntaxe : fonctionSQL(champ) AS nomChamp

Inconvénient de cette première solution

Donc la date de la 'new' est éclatée dans trois champs calculés.
Le script d'affichage de l'info courante doit reprendre ces trois champs.

Deuxième solution

Il y a beaucoup plus simple ! Il suffit d'utiliser la fonction SQL date_format.
La requête SQL devient :

SELECT date_format(date_new,'%d-%m-%Y') AS date_info, titre, message  
			FROM news ORDER BY ID DESC ; 

Ainsi le contenu du champ calculé date_info sera affiché sous la forme JJ-MM-AAAA car le format d'affichage (deuxième argument de la fonction) est : %d-%m-%Y
%Y veut dire affichage de la date sur 4 chiffres (%y : affichage sur deux chiffres).

Le script sera alors beaucoup plus simple :

Les requêtes sélection monotables

Dans ce paragraphe je vous présente des requêtes sélection monotables de plus en plus complexes.
Ces requêtes porteront sur la table "membres" qui est présentée ci-dessous?

La table 'membres'

Dans la base de données du site de l'association il y a la table "news" mais il existe aussi la table "membres" (qui reprend tous les adhérants au club).

Structure de la table "membres"

nom col.typetailleautres attributs
idintAUTO_INCREMENT PRIMARY_KEY
nom varchar30NOT NULL
prenomvarchar20NOT NULL
sexevarchar1NOT NULL DEFAULT 'M'
adresse1varchar50NULL
adresse2varchar50NULL
adresse3varchar50NULL
tel1varchar14NULL
tel2varchar14NULL
bureautinyintNOT NULL DEFAULT 0
date_naissdateNOT NULL
mailvarchar50NULL

Remarque que certaines colonnes l'attribut NULL et d'autres NOT NULL.
Si un champ à l'attribut NOT NULL cela signifie qu'il faut affecter un valeur à ce champ lors de l'ajout d'un nouvel enregistement.
Par contre si l'attribut est à NULL et lors de l'insertion d'un nouvel enregistrement, il n'est pas obligatoire d'affecter une valeur à ce champ.
Il y a aussi l'attribut DEFAULT. Ainsi si on n'a pas rempli le champ BUREAU à l'occasion d'un ajout de ligne, c'est la valeur 0 qui lui sera affectée.

Contenu de la table

Certaines colonnes sont ici masquées ; il s'agit des champs : adresse1, adresse2, adresse3 , tel1 et tel2

idnomprenomsexebureaudate_naiss
1DupontJulienM01957-04-28
2BailleulHenrietteF11967-03-07
3DupuisEricM01980-02-08
4FrançoisHélèneF11947-01-09
5DumoulinJacquesM11970-06-15
6MillienjérémyM01977-09-13

Le champ sexe contient "M" ou "F".
Le champ "bureau" contient 1 (membre du bureau) ou 0 (non membre du bureau).

Les requêtes sélection sur cette table

Ces requêtes sélection portant sur le tables "membres" seront de plus en plus complexes.

Afficher toutes les lignes (ou enregistrements) et colonnes (ou champs)

SELECT * FROM membres ;
Les enregistrements sont affichés dans l'ordre de saisie.
Notez que les mots "select" & "from" sont en majuscules.

Il est d'usage d'écrire les mots réservés du langage SQL en majuscules. Ce n'est pas une obligation mais la lisibilité du code en est améliorée.

La clause ORDER BY

Elle permet d'afficher les lignes selon un ordre différent de l'ordre de saisie donc d'effectuer un tri.

Afficher les membres dans l'ordre alphabétique

SELECT * FROM membres ORDER BY nom ;
ou mieux encore :
SELECT * FROM membres ORDER BY nom, prenom ;

Afficher du plus jeune au plus vieux

Le membre le plus jeune a la date de naissance la plus forte ; le plus âgé la date de naissance la plus faible.
Il faut donc afficher selon la colonne date_naiss par valeurs décroissantes.
SELECT * FROM membres ORDER BY date_naiss DESC ;
DESC : comme "descending" (ou décroissant en Français).

La clause WHERE

Grâce à cette clause on n'affiche que les enregistrements vérifiant une condition. Il s'agit donc d'un filtrage.

Afficher uniquement les membres du bureau

Le contenu du champ "bureau" vaut 1 pour les membres du comité de direction.
SELECT * FROM membres WHERE bureau = 1 ;

Afficher les membres de sexe féminin

SELECT * FROM membres WHERE sexe ="F" ;

Sélection de colonnes (ou champs)

Jusqu'à présent le mot clé SELECT est suivi de * donc on affiche tous les champs.
Pour n'afficher que certains champs il suffit de remplacer * par la liste des champs que l'on veut afficher.

Liste des membres du bureau par ordre alphabétique

SELECT nom, prenom, sexe, tel1, tel2 FROM membres WHERE bureau ="1" ORDER BY nom, prenom ;

Utiliser des fonctions SQL dans la requête

Nous avons déjà vu les fonctions DAY(),MONTH(),YEAR(), DATE_FORMAT() mais il existe aussi des fonctions que l'on peut appliquer à des chaines.

Afficher les noms en majuscules et les prénoms en minuscules

SELECT UPPER(nom)AS nom, LOWER(prenom)AS prenom, DATE_FORMAT(date_naiss,'%d-%m-%Y') AS date_naissance FROM membres ;
La liste retournée :

nom				prenom			sexe		date_naiss	
DUPONT			julien			M			1957-04-28	
BAILLEUL		henriette		F			1967-03-07	
DUPUIS			eric			M			1980-02-08	
FRANÇOIS		hélène			F			1947-01-09	
DUMOULIN		jacques			M			1970-06-15	
MILLIEN			jérémy			M			1977-09-13	

Les noms sont affichés en majuscules grâce à la fonction UPPER() et les prénoms en minuscules avec la fonction LOWER().

Le filtrage avec la clause WHERE

La clause WHERE peut être suivie par une expression complexe pour obtenir des conditions de filtrage sophistiquées.

Affichez les membres dont le nom commence par la lettre D

Solution 1

SELECT * FROM membres WHERE nom LIKE 'D%' ;
Le caractère % est un joker ; il remplace un nombre variable de caractères.
Attention dès que vous utiliser le joker % il faut utiliser l'opérateur LIKE (et non pas =).

Solution 2 :

SELECT * FROM `membres` WHERE LEFT(nom,1) ='D';

Emploi de la fonction LEFT(). Ici le deuxième argument de la fonction est 1 donc extraction du premier caractère de chaque nom pour comparer avec D.

MySQL n'est pas sensible à la casse ; la requête retourne toutes les lignes dont le nom commence par 'd' ou 'D' !

Affichez les membres dont le nom commence par la lettre D ou par la lettre M

SELECT * FROM membres WHERE nom LIKE 'D%' OR nom LIKE 'M%' ; 
	Ou
SELECT * FROM `membres` WHERE LEFT(nom,1) ='D' OR LEFT(nom,1) ='M'; 

L'expression de filtrage comprend un opérateur logique, ici OR.

Afficher les membres du bureau de sexe féminin

SELECT * FROM `membres` WHERE bureau =1 AND sexe ='F'

Ici l'opérateur logique est AND.

Liste des membres dont le nom commence par n'importe quelle lettre autre que D

SELECT * FROM membres WHERE nom NOT LIKE 'D%'; 
	Ou
SELECT * FROM membres WHERE LEFT(nom,1) != 'D'; 

Ici emploi de l'opérateur logique NOT LIKE dans la première solution et != (différent) dans la deuxième solution.

L'étendue avec la clause LIMIT

Imaginez que la table "news" contiennent des centaines de lignes mais on ne veut afficher que les 10 dernières infos.
SELECT * FROM news ORDER BY id desc LIMIT 10;
Les lignes triées par valeurs décroissantes d'id (donc d'abord les plus récentes) et le moteur SQL ne prend en compte que les dix premières dans cet ordre.

Requêtes sélection paramétrées

Dans le cadre d'une requête sélection les valeurs des conditions WHERE peuvent être des variables PHP suite à une soumission.

Je vous propose une TP : connexion d'un inscrit à son espace personnel.
J'ai rajouté dans la base chez mon hébergeur, une troisième table : "inscrits".

La table "inscrits"

Ci-dessous des extraits du script SQL produit par le programme PHPMyadmin de mon hébergeur lorsque je demande une exportation de la table "inscrits".
En guise de structure je présente la requête CREATE TABLE et en guise de contenu je vous communique la requête INSERT.

Structure de la table

CREATE TABLE `inscrits` (
  `identifiant` varchar(30) NOT NULL PRIMARY KEY,
  `motpasse` varchar(10) NOT NULL,
  `nom` varchar(30) NOT NULL,
  `prenom` varchar(20) NOT NULL,
  `date_naiss` date NOT NULL ; 
)

Les identifiants sont uniques puisque ce champ est clé primaire.

Contenu de la table 'inscrits'

INSERT INTO `inscrits` (`identifiant`, `motpasse`, `nom`, `prenom`, `date_naiss`) VALUES
('dupont@free.fr', 'zoro5362', 'Dupont', 'Julien', '1955-10-17'),
('dupuis@aol.com', 'sesame59', 'Dupuis', 'Léon', '1965-10-17'),
('lecornu@matignon.fr', 'censure26', 'Lecornu', 'Sébastien', '1975-10-17'),
('macron@elysee.com', 'toxique75', 'Macron', 'Emmanuel', '1985-10-17');

Dans la réalité il y aurait plusieurs milliers de lignes et les mots de passe seraient cryptés ('hachés').
Concernant la technique de 'hachage' revoyez le chapitre : Les fonctions PHP

La page "connexion_inscrits.php"

Il y a bien sûr un premier script PHP pour l'inclusion des fichiers "commun.php" & "connexion.php" (voir chapitre précédent).

Le deuxième script de la page

Observez bien la requête sélection paramétrée et en particuliere ce qui suit la clause WHERE :
... WHERE identifiant = '$identifiant_saisi' AND motpasse ='$motpasse_saisi'
À gauche du signe égal un nom de champ et à droite une variable PHP
$record contient un seul enregistrement OU n'existe pas !
Donc si (!$record) retourne true alors un messge informe le visiteur qu'il a mal renseigné le formulaire. Sinon affichage personnalisé dans la page : afficher nom et prénom de l'inscrit connecté.

Le rendu dans un nouvel onglet

Saisissez dans le formulaire l'adresse mail et le mot de passe de l'un des quatre inscrits (voir le contenu de la table "inscrits plus haut dans ce chapitre).
Testez la page de connexion d'un inscrit à son espace perso

Dans un monde de bisounours ce code serait acceptable mais dans la réalité il présente une faille de sécurité.
Je n'en dis pas plus ; voir la page en lien : Utilisation avancée de MYSQL

La recherche indexée

Dans la table "inscrits" la colonne "identifiant" est clé primaire. Donc un index est créé sur cette colonne.

La recherche indexée, c'est ce que vous pratiquez lorsque vous recherchez une information dans un gros ouvrage technique.

Imaginez que vous disposez d'un ouvrage sur PHP & MYSQL" de plus de 800 pages.
Vous avez créé une requête MAJ mais elle retourne un message d'erreur. Vous en déduisez à juste titre que vous ne maitrisez pas les requêtes UPDATE. Il vous faut donc "plonger" dans la documentation.
Vous n'allez pas lire une à une les pages du volumineux manuel informmatique jusqu'à trouver le chapitre portant sur les requêtes SQL action (dont UPDATE). C'est beaucoup plus rapide de lire l'index du livre. Ce dernier comprend l'entrée "update" qui vous renvoie à la page 310. Vous réouvrez alors le livre et allez directement à la page 310.

Dès que vous lancez une requête sélection sur une table indexée, la recherche est implicitement indexée.

Dans l'exemple, imaginez que vous ayez tapé en guise d'identifiant 'lecornu@matignon.fr'.
SQL procède à un scan de l'index. Celui-ci lui indique qu'il s'agit du troisième enregistrement ; SQL accède alors directement à l'enregistrement 3.

Requêtes multi-tables

Une reqête multitables extrait des données de plusieurs tables reliées entre elles.

Les données à manipuler

Le bureau de l'association vous demande maintenant de créer au sein du site du club un "forum". En effet avant de prendre un décision importante, le bureau souhaite pouvoir consulter les autres membres, leur demander leur avis au travers d'un forum.

Utilisation d'une seule table

La structure de la table serait : un enregistrement par commentaire de billet.

Donc si pour un billet il y a 10 commentaire on aura 10 lignes avec le même contenu pour les champs :

On ne peut pas dire que cette organisation des données soit judicieuse ...

Utilisation de deux tables

Pour éviter les répétitions de données inévitables dans le cadre d'une table unique il faut mieux utiliser deux tables  avec une relation entre elles grâce à un champ commun aux deux tables.

Ci-dessous je vous communique des extraits du script d'exportation produit par PHPMYADMIN chez mon hébergeur.
Donc pour indiquer la structure d'une table je vous communique le code d'une requête CREATE TABLE. Et pour indiquer le contenu d'une table je vous présente le code de la requête INSERT.

La table "billets"

Structure de cette table
CREATE TABLE `billets` (
  `id_billet` int(3) AUTO_INCREMENT PRIMARY KEY,
  `titre` varchar(30) NOT NULL,
  `date_billet` date NOT NULL,
  `contenu` text NOT NULL
); 

Le champ id_billet est "auto_increment" (remplissage automatique du champ : incrémentation de 1). Bien sûr on ne peut appliquer l'attribut AUTO_INCREMENT qu'à un champ numérique.

Le contenu de cette table

Cette table contient 4 billets.
Constatez que les apostrophes sont échappés (\')

Vous avez aussi des 'bizarreries' :
\n : pour saut de ligne
\r : pour un retour chariot

La table "commentaires"

Structure de la table "commentaires"
CREATE TABLE `commentaires` 
(
  `id_commentaire` int(11) AUTO_INCREMENT PRIMARY KEY,
  `id_billet` int(3) DEFAULT NULL,
  `auteur` varchar(30) NOT NULL,
  `commentaire` text NOT NULL
); 

Le champ id_billet peut être vide.

Le contenu de cette table

La relation entre les deux tables

Dans la table "billets" il existe le champ "id_billet" qu'on retrouve dans la table "commentaires".
Dans ces deux champs l'information est identique : le numéro du billet.
Ici ces deux champs portent le même nom mais ce n'est pas une obligation.

Nous allons pouvoir poser une jointure entre le champ "billet.id_billet" et le champ "commentaires.id_billet".
Ci-dessus j'ai utilisé les noms complets des champs : nomTable.nomChamp.
Grâce à cette jointure on peut alors associer à chaque enregistrement de "billets" 0 à N enregistrements de "commentaires".
En effet dans la table "billets" le champ "id_billet" est clé primaire (doublons interdits). alors que dans la table "commentaires" le champ "id_billet" est simplement entier ou NULL.

Les requêtes sélection multi-tables

Elles permettent d'extraire des données appartenant à plusieurs tables.

Ci-dessous je vous présente trois états (un état est une page web affichant le résultat d'une requête sélection).
Ces trois états ont chacun pour source une requête sélection sur les tables "billets" & "commentaires".

État ayant pour source une jointure interne

Le code de l'état "jointure_inner.php"

Notez bien dans le code de la requête :

SELECT billets.id_billet AS idb, id_commentaire, titre, contenu, 
date_format(date_billet,'%d-%m-%Y')AS date_billet, auteur, commentaire
FROM billets INNER JOIN commentaires ON billets.id_billet = commentaires.id_billet 
ORDER BY billets.id_billet

La source de l'état est une requête multitables de type "INNER JOIN".
Notez que j'ai créé un alias pour le champ "billets.id_billet" : idb

Le plus important dans ce code SQL est la clause FROM qui indique comment on relie à chaque enregistrement de "billets" 0 à N lignes de "commmentaires" ; il doit y avoir égalité sur les champs de jointure.

Le rendu

La requête source retourne 4 lignes.

Problème : les lignes de "billets" n'ayant pas de correspondant dans "commentaires" ne sont pas pris en compte !
Donc aucune mention dans la requête (et donc dans l'état) des billets 1 et 4.
Le commentaire numéroté 5 qui n'est pas relié à un billet n'est pas pris en compte également.

Requête avec jointure externe gauche

Toutes les lignes de la table "billets" sont prises en compte même s'elles n'ont pas "d'enfants" dans la table "commentaires".

Le code de l'état "jointure_gauche.php"

Seule l'instruction relative à l'appel de requête change par rapport à l'état précédent :

...
$selection = $bdd->query("SELECT billets.id_billet as idb,id_commentaire, titre, contenu, 
date_format(date_billet,'%d-%m-%Y')AS date_billet, auteur, commentaire
FROM billets LEFT JOIN commentaires 
ON billets.id_billet = commentaires.id_billet 
ORDER BY billets.id_billet ;") ;
...

Le code de la requête est identique à la précédente sauf :

...
FROM billets LEFT JOIN commentaires ...
...

La table "billets" est à gauche de la jointure "LEFT JOIN" donc cela veut dire que la requête prend en compte toutes les lignes de la table "billets" même celles qui n'ont pas 'd'enfant' dans la table "commentaires".

Le rendu

Six lignes !
Toutes les lignes de la table "billets" sont pris en compte.
Pour un billet sans commentaire, la requête retourne quand même une ligne avec certains champs vides (auteur, commentaire).
Pour un billet avec N commentaires, la requête retourne N lignes.

Jointure externe droite

Pour que toutes les lignes de la table "commentaires" soient prises en compte, même celles qui n'ont pas de 'parent' dans la table 'billets', il faut réaliser une jointure externe droite.

Le code de l'état "jointure_droite.php"

Seule l'instruction relative à l'appel de requête change par rapport au premier état :

...
$selection = $bdd->query("SELECT billets.id_billet as idb, id_commentaire, titre, contenu, 
date_format(date_billet,'%d-%m-%Y')AS date_billet, auteur, commentaire
FROM billets RIGHT JOIN commentaires 
ON billets.id_billet = commentaires.id_billet 
ORDER BY billets.id_billet;") ;
...

Le code de la requête est identique à la précédente sauf :

...
FROM billets RIGHT JOIN commentaires ...
...

La table "commentaires" est à droite de "billets" donc cela veut dire que la requête prend en compte toutes les lignes de la table "commentaires".

Le rendu

Cinq lignes.
Tous les enregistrements de la table "commentaires" sont pris en compte. Pour un commentaire sans billet les champs id_billet, date_billet,titre, contenu sont vides.

Remarques

Concernant les requêtes

Les requêtes externes gauche sont les plus fréquentes.
Les requêtes externe droites sont utiles pour détecter les 'orphelins'.
Ainsi pour reprendre ce thème, il est anormal qu'il y ait un commentaire associé à aucun billet. Cela indique une faille de sécurité !

Concernant les états

Cette redondance d'informations nuit à la lisibilité de l'état.
Rassurez-vous, il y a une solution.

Utilisation avancée de MYSQL