Vous pouvez me contacter via Facebook pour questions & suggestions :
Page Facebook relative à mon site
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.
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.
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.
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
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.
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 :
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?
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).
| nom col. | type | taille | autres attributs |
|---|---|---|---|
| id | int | AUTO_INCREMENT PRIMARY_KEY | |
| nom | varchar | 30 | NOT NULL |
| prenom | varchar | 20 | NOT NULL |
| sexe | varchar | 1 | NOT NULL DEFAULT 'M' |
| adresse1 | varchar | 50 | NULL |
| adresse2 | varchar | 50 | NULL |
| adresse3 | varchar | 50 | NULL |
| tel1 | varchar | 14 | NULL |
| tel2 | varchar | 14 | NULL |
| bureau | tinyint | NOT NULL DEFAULT 0 | |
| date_naiss | date | NOT NULL | |
| varchar | 50 | NULL |
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.
Certaines colonnes sont ici masquées ; il s'agit des champs : adresse1, adresse2, adresse3 , tel1 et tel2
| id | nom | prenom | sexe | bureau | date_naiss |
|---|---|---|---|---|---|
| 1 | Dupont | Julien | M | 0 | 1957-04-28 |
| 2 | Bailleul | Henriette | F | 1 | 1967-03-07 |
| 3 | Dupuis | Eric | M | 0 | 1980-02-08 |
| 4 | François | Hélène | F | 1 | 1947-01-09 |
| 5 | Dumoulin | Jacques | M | 1 | 1970-06-15 |
| 6 | Millien | jérémy | M | 0 | 1977-09-13 |
Le champ sexe contient "M" ou "F".
Le champ "bureau" contient 1 (membre du bureau) ou 0 (non membre du bureau).
Ces requêtes sélection portant sur le tables "membres" seront de plus en plus complexes.
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.
Elle permet d'afficher les lignes selon un ordre différent de l'ordre de saisie donc d'effectuer un tri.
SELECT * FROM membres ORDER BY nom ;
ou mieux encore :
SELECT * FROM membres ORDER BY nom, prenom ;
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).
Grâce à cette clause on n'affiche que les enregistrements vérifiant une condition. Il s'agit donc d'un filtrage.
Le contenu du champ "bureau" vaut 1 pour les membres du comité de direction.
SELECT * FROM membres WHERE bureau = 1 ;
SELECT * FROM membres WHERE sexe ="F" ;
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.
SELECT nom, prenom, sexe, tel1, tel2 FROM membres WHERE bureau ="1" ORDER BY nom, prenom ;
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.
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().
La clause WHERE peut être suivie par une expression complexe pour obtenir des conditions de filtrage sophistiquées.
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 =).
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' !
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.
SELECT * FROM `membres` WHERE bureau =1 AND sexe ='F'
Ici l'opérateur logique est AND.
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.
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.
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".
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.
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.
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
Il y a bien sûr un premier script PHP pour l'inclusion des fichiers "commun.php" & "connexion.php" (voir chapitre précédent).
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é.
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
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.
Une reqête multitables extrait des données de plusieurs tables reliées entre elles.
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.
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 ...
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.
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.
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
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.
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.
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".
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.
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.
Toutes les lignes de la table "billets" sont prises en compte même s'elles n'ont pas "d'enfants" dans la table "commentaires".
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".
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.
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.
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".
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.
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é !
Cette redondance d'informations nuit à la lisibilité de l'état.
Rassurez-vous, il y a une solution.