Vous êtes sur la page 1sur 8

Correction TD2

Cette correction, adaptée à PL/SQL, est principalement à mettre au crédit de Cécile Capponi,
responsable du cours Base de Données en télé-enseignement. Des fichiers créant la base ainsi
que quelques données se trouvent à l'adresse http://www.lif.univ-
mrs.fr/~reyraud/BD/Hotel_Oracle.tar.gz.

Toutes les corrections indiquées ici sont des exemples de corrections : il existe toujours d'autres
possibilités. Par ailleurs, n'ayons pas traité en cours la récupération des erreurs SQL dans le cadre
des fonctions PL/SQL, les corrections données ici supposent qu'aucune erreur SQL n'est produite.
Or, dans certains cas, des erreurs peuvent survenir : nous ne les traitons pas, mais une
implantation parfaite des fonctions devrait anticiper toute erreur SQL et la traiter.
Nous travaillons sur la base dont le schéma est le suivant :
hotel(numhotel, nom, ville, etoiles)
chambre(numchambre, numhotel, etage, type, prixnuitht)
client(numclient, nom, prenom)
occupation(numoccup, numclient, numchambre, numhotel, datearrivee, datedepart)
reservation(numresa, numclient, numchambre, datearrivee, datedepart)
Question 1. Supprimer tous les clients n'ayant jamais occupé de chambre, et n'ayant aucune
réservation en cours.
Le plus "difficile" ici est d'écrire la condition de suppression des clients. Il s'agit en fait de
supprimer les clients qui n'en sont pas vraiment, puisqu'ils n'ont jamais occupé de chambre et
qu'ils n'en réservent pas. Il s'agit donc des clients dont le numéro n'apparaît dans aucune
réservation ni dans aucune occupation de chambre. La requête suivante nous renvoie le tableau
des numéros de clients qui n'apparaissent dans aucune des tables occupation et reservation.

SELECT numclient FROM client


MINUS
( SELECT numclient FROM occupation
UNION
SELECT numclient FROM reservation );

Il nous reste donc à effacer les clients dont le numéro est une ligne de la relation ci-dessus. Pour
cela, nous exécutons la commande (en italique, nous indiquons où se trouve la sélection des
clients concernés) :
DELETE FROM client
WHERE numclient IN ( SELECT numclient FROM client
MINUS
( SELECT numclient FROM occupation
UNION
SELECT numclient FROM reservation ) );

Il est possible (et meilleur) de faire un petit peu plus simple, en considérant que la condition sur
le DELETE peut être formée d'un NOT IN. Dans ce cas, le MINUS n'est pas requis dans la requête de
sélection des numéros de clients, seule la seconde partie est valable : on supprime les clients dont
le numéro n'est pas dans les numéros de clients ayant effectué une réservation ou occupé une
chambre :
DELETE FROM client
WHERE numclient NOT IN ( SELECT numclient FROM occupation
UNION
SELECT numclient FROM reservation ) );

Question 2. Ajouter une étoile à l'Hotel terminus de Grenoble.


Pour ajouter une étoile, il faut utiliser la commande UPDATE qui ajoute 1 à la valeur courante de
etoiles pour le tuple vérifiant la condition sur le nom et la ville.
UPDATE hotel SET etoiles = etoiles+1
WHERE nom = 'Hotel terminus' AND ville='Grenoble';

Question 3. Augmenter de 12% le prix de toutes les chambres à Nice.

Là aussi nous utilisons la commande UPDATE sur les tuples de la table chambre. La sélection
des chambres concernées (celles de Nice) nécessite la sélection de tous les hôtels de Nice : la
clause WHERE vérifie que le numéro d'hôtel de la chambre à modifier fait bien partie du tableau des
numéros d'hôtels dont la ville est 'Nice'. Il est inutile de passer par les numéros de chambres,
seule une condition sur le numéro de l'hôtel est suffisante.
UPDATE chambre SET prixnuitht = prixnuitht + prixnuitht*12/100
WHERE numhotel IN ( SELECT numhotel FROM hotel WHERE ville = 'Nice');

Question 4. Ecrire une fonction qui accepte en paramètre le nom et la ville d'un l'hôtel et qui
retourne le nombre d'étoiles de cet hôtel.

La principale difficulté dans l'écriture de cette fonction est le type de ses paramètres et son type
de retours. Comme nous ne les connaissons pas de manière détaillée, nous utilisons %TYPE. A
noter que l'on ne peut pas retourner directement le résultat d'une requête et qu'il nous faut donc
utiliser une variable (noter resultat ici)
CREATE OR REPLACE FUNCTION nb_etoiles(nom_hotel hotel.nom%TYPE,
ville_hotel hotel.ville%TYPE)
RETURN hotel.etoile%TYPE IS
resultat hotel.etoile%TYPE; -- déclaration de la variable
BEGIN
SELECT etoile INTO resultat
FROM hotel
WHERE nom = nom_hotel AND ville = ville_hotel;
RETURN resultat;
END;

Question 5.
● Ajouter deux colonnes dans la table hotel, pour stocker les congés annuels de l'hôtel : ces
colonnes doivent stocker la date de début et la date de fin du congé annuel.
ALTER TABLE hotel
ADD debutconges Date DEFAULT CURRENT_DATE();
ALTER TABLE hotel
ADD finconges Date DEFAULT CURRENT_DATE();
Afin de ne pas se retrouver avec des colonnes ne contenant que la valeur NULL, on peut spécifier que la
date par défaut soit celle d'aujourd'hui.
Dans un esprit de gestion de la cohérence de la base, il est bon de rajouter une contrainte vérifiant que la
date de fin de congés soit postérieure à celle du début des congés :
ALTER TABLE hotel
ADD CONSTRAINT contrainte_posterite CHECK (debutconges <= finconges );

● Ajouter une contrainte à cette table hotel empêcher les congés annuels de dépasser 21 jours.
ALTER TABLE hotel
ADD CONSTRAINT contrainte_conges CHECK (debutconges-finconges < 21);
● Modifier en conséquence les données, en prenant pour principe que les hôtels de Nice sont fermés
entre le 22/01 et le 10/02, que ceux des Alpes sont fermés du 1/08 au 18/08, et que les autres
ferment les 21 premier jours de novembre.

On remarque que Grenoble est la seule ville des Alpes présente dans la table hotel, le reste du travail
consiste en des UPDATE classiques.

UPDATE hotel SET debutconges = TO_DATE('21/01', 'dd/mm'),


finconges = TO_DATE('10/02', 'dd/mm'),
WHERE numhotel IN (SELECT numhotel FROM hotel WHERE ville='Nice');
UPDATE hotel SET debutconges = TO_DATE('01/08', 'dd/mm'),
finconges = TO_DATE('18/08', 'dd/mm'),
WHERE numhotel IN (SELECT numhotel FROM hotel WHERE ville='Grenoble');
UPDATE hotel SET debutconges = TO_DATE('01/11', 'dd/mm'),
finconges = TO_DATE('21/11', 'dd/mm'),
WHERE numhotel NOT IN (SELECT numhotel FROM hotel WHERE ville='Grenoble'
UNION
SELECT numhotel FROM hotel WHERE ville='Nice');

Question 6.
Créer la table facture constituée des colonnes suivantes : num_facture, date_facture, num_occupation,
montant_ht, payee. Expliquez pourquoi ces données suffisent pour imprimer une facture pour un
séjour, avec nom du client, dates du séjour, etc. Si vous désirez ajouter d'autres colonnes, faites le
mais en justifiant votre choix.

(1) Pour créer la table, on utilise la commande CREATE TABLE en spécifiant chaque colonne. On
réfléchit aux éventuelles contraintes pouvant être posées sur ces colonnes : il y a la contrainte de
clé primaire que nous posons naturellement sur num_facture, le montant de la facture que nous
imposons positif ou nul, la date non vide (et par défaut la date du jour de création de la facture),
une contrainte de clé étrangère sur num_occupation qui référence la même colonne -- numoccup --
dans la table occupation. Nous imposons aussi que ce numéro d'occupation soit non vide, et unique
car deux factures ne peuvent concerner la même occupation.

CREATE TABLE facture


(
num_facture NUMBER NOT NULL CONSTRAINT cp_facture PRIMARY KEY,
date_facture date NOT NULL DEFAULT CURRENT_DATE,
num_occupation NUMBER NOT NULL UNIQUE,
montant_ht NUMBER CONSTRAINT ch_montant CHECK (montant_ht > 0),
payee bool DEFAULT false,
CONSTRAINT ce_facture_occup FOREIGN KEY (num_occupation)
REFERENCES occupation(numoccup)
);

(2) Ces données suffisent car elles permettent de retrouver dans la base toutes les données
nécessaires à l'établissement de la facture : le numéro d'occupation permet d'accéder au client
(puisque la table occupation référence le client), à l'hôtel (puisque la table occupation référence l'hôtel)
et au prix (puisque la table occupation référence la chambre, donc le prix par nuit). Par ailleurs, les
dates du séjour sont aussi présentes dans la table occupation. Donc tout y est !

Pour chaque séjour terminé, créer une ligne de la table facture, avec la date courante comme date
d'édition de la facture (le cas du numéro de facture ne sera pas forcément traité).

Nous allons utiliser deux fonctions : une première qui créé une facture à partir du numéro
d'occupation, une seconde qui génère toutes les factures à l'aide de la table occupation. Un
problème se pose que nous ne gérons pas ici : normalement nous devons attribuer
automatiquement à chaque facture un numéro différent. Pour ce faire, il nous faudrait utiliser deux
structures que nous ne connaissons pas encore : les triggers et les séquences. Nous décidons
donc de “botter en touche” en considérant dans la première fonction que le numéro de la facture à
créer est un paramètre et, dans la seconde fonction, en prenant comme numéro de facture le
numéro de la données dans la table occupation. Cette façon de faire pose bien entendu problème
car on ne saura pas quel numéro attribuer ultérieurement à une nouvelle facture...

CREATE OR REPLACE FUNCTION creation_facture(nocc occupation.numoccup%TYPE, nfact NUMBER)


RETURN NUMBER IS
-- nocc: num d'occupation, nfact: num de facture

cout_chambre chambre.prixnuitht%TYPE; -- le prix de la chambre qui a été occupée


une_occup occupation%ROWTYPE; -- l'occupation en cours de traitement (1 ligne)
prix_sejour NUMBER(4); -- le prix total du séjour
la_facture NUMBER; -- numéro de la facture de l'occupation n° nocc
BEGIN
-- On vérifie que la facture n'existe pas déjà pour cette occupation
SELECT num_facture INTO la_facture FROM facture WHERE num_occupation = nocc;
IF ( la_facture IS NULL ) -- On crée la facture que si elle n'existe pas déjà
THEN
-- Récupération de l'occupation à partir de son identifiant
SELECT * INTO une_occup FROM occupation WHERE occupation.numoccup = nocc;
-- Récupération du prix de la chambre occupée à partir de son identifiant
SELECT prixnuitht INTO cout_chambre FROM chambre
WHERE chambre.numchambre = une_occup.numchambre;
-- Calcul du prix total HT du séjour
prix_sejour := ( une_occup.datedepart - une_occup.datearrivee ) * cout_chambre ;
-- Insertion de la facture avec les données actuelles
INSERT INTO facture (num_facture, num_occupation, montant_ht)
VALUES (nfact, nocc, prix_sejour);
la_facture := nfact;
END IF;
RETURN nfact ; -- Retour du numéro de la facture créée ou récupérée
END;

CREATE OR REPLACE FUNCTION creations_factures() RETURN NUMBER IS


une_occup occupation%ROWTYPE; -- Les données de l'occupation en cours de traitement
CURSOR les_occupations IS SELECT * FROM occupation; -- Toutes les occupations
nbfact NUMBER := 0; -- Le nombre de factures créées
BEGIN
OPEN les_occupations; -- Récupération de toutes les occupations
FETCH les_occupations INTO une_occup; -- Début de parcours des occupations
WHILE les_occupations%FOUND LOOP
-- On cherche si l'occupation est valide
IF une_occup.datedepart IS NOT NULL AND une_occup.datedepart <= CURRENT_DATE
THEN -- l'occupation est valide : création de la facture
DECLARE
numf NUMBER; -- Le n° de facture: ne sert à rien ici...
BEGIN -- Appel à la fonction de création individuelle de facture
numf := creation_facture(une_occup.numoccup, nbfact);
nbfact := nbfact+1; -- incrémentation du nb total de factures créées
END;
END IF;
FETCH les_occupations INTO une_occup; -- On passe à l'occupation suivante
END LOOP;
CLOSE les_occupations; -- Fermeture des occupations: il n'y en a plus
COMMIT; -- modification effective de la base
RETURN nbfact;
END;

Créer une fonction qui retourne le texte permettant l'édition d'une facture pour le client. Sur la
facture, les détails du séjour doivent apparaître, ainsi que le nom de l'hôtel, nom et prénom du
client, etc.
CREATE OR REPLACE FUNCTION edition_facture(nfacture NUMBER) RETURN VARCHAR IS
DECLARE
une_facture facture%ROWTYPE; -- La facture à éditer
une_occup occupation%ROWTYPE; -- L'occupation concernée
un_client client%ROWTYPE; -- Le client concerné
un_hotel hotel%ROWTYPE; -- L'hôtel concerné
numch chambre.numchambre%TYPE; -- Le numéro de la chambre
text VARCHAR; -- Le texte final
paye VARCHAR; -- 'oui' si facture payée, 'non' sinon
BEGIN
-- Récupération de la facture
SELECT * INTO une_facture FROM facture WHERE facture.num_facture = nfacture;
-- Récupération de l'occupation
SELECT * INTO une_occup FROM occupation
WHERE une_facture.num_occupation = occupation.numoccup;
-- Récupération du client
SELECT * INTO un_client FROM client WHERE client.numclient = une_occup.numclient;
-- Récupération de l'hôtel
SELECT * INTO un_hotel FROM hotel WHERE hotel.numhotel = une_occup.numhotel;
-- Récupération du numéro de chambre
SELECT * INTO numch FROM chambre WHERE chambre.numchambre = une_occup.numchambre;
-- Récupération de l'information sur le paiement
IF une_facture.payee THEN paye = 'oui';
ELSE paye := 'non';
END IF;
-- Construction de l'édition : un \n est un retour-chariot, et \t est une tabulation
text := un_hotel.nom + ' à ' + un_hotel.ville + '\n'
'Facture adressée à ' + un_client.nom + ' ' + un_client.prenom + ' (numéro: '
+ un_client.numclient + ')\n' +
'Date arrivée: ' + TO_CHAR(une_occup.datearrivee, 'DD-MM-YYYY') + '\n' +
'Date départ : ' + TO_CHAR(une_occup.datedepart, 'DD-MM-YYYY') + '\n'
'N° chambre occupée : ' + numch + '\n' +
'Nombre nuit(s) : ' + une_occup.datedepart-une_occup.datearrivee + '\n' +
'Montant HT = ' + une_facture.montant_ht + '\n'
'Montant TTC = ' + une_facture.montant_ht + une_facture.montant_ht/100*19.6
+ '\n' +
'Réglé : ' + paye + '\n \t \t A ' + un_hotel.ville +
' le ' + TO_CHAR(une_facture.date_facture, 'DD-MM-YYYY');
RETURN text; -- Retour du texte édité
END;

Question 7.
Ecrire une fonction qui accepte en paramètre le nom et la ville d'un l'hôtel et qui retourne le prix
moyen des chambres dans cet hôtel.

Cette question n'est pas plus compliquée que les précédentes à l'exception de l'utilisation d'un
curseur avec des paramètres. En effet, il nous faut récupérer toutes les chambres de l'hotel qui
nous intéresse pour calculer la somme des prix et le nombre de chambres et en déduire la
moyenne ; or l'hotel et sa ville sont les paramètres de la fonction, donc du curseur.

CREATE OR REPLACE FUNCTION prix_moyen(nom_hotel VARCHAR, ville_hotel VARCHAR)


RETURN NUMBER IS
nb_chambres := 0;
somme_prix :=0 ;
une_chambre chambre%ROWTYPE;
CURSOR les_chambres(n_h VARCHAR, n_v VARCHAR) IS
SELECT numchambre, numhotel, etage, type, prixnuitht
FROM chambre JOIN hotel USING(numhotel)
WHERE hotel.nom = n_h AND hotel.ville = n_v;
BEGIN
OPEN les_chambres (nom_hotel, ville_hotel);
FETCH les_chambres INTO une_chambre;
WHILE (les_chambres%FOUND) LOOP
nb_chambres := nb_chambres +1;
somme_prix := somme_prix + une_chambre.prixnuitht;
END LOOP;
CLOSE les_chambres;
RETURN somme_prix / nb_chambres;
END;

Question 8. Créer une fonction de demande de réservation : cette fonction prend en paramètres
la ville du séjour, le nom de l'hôtel, la date de début de séjour et la date de fin, et le type de
chambre à réserver, et enfin le numéro de client. La fonction consulte alors les chambres
disponibles de ce type, et crée la réservation si c'est possible. Si la réservation a été créée, la
fonction retourne le numéro de la commande créée, sinon la fonction renvoie -1.

Cette fonction demande un peu de méthode pour être écrite : dans un premier temps, on
récupère l'identifiant de l'hôtel concerné (lhotel) à l'aide de son nom et de la ville : on utilise pour
cela un SELECT INTO, et même s'il existe plusieurs hôtels portant le même nom dans la même ville,
on ne garde ainsi que le premier retourné. Notons ici que si un tel hôtel n'existe pas, la fonction se
contente de retourner -1 (on teste que le SELECT INTO renvoie un numéro d'hôtel grâce à
l'instruction IF ( lhotel IS NOT NULL ) placée juste après la commande SELECT.
Une fois l'identifiant de l'hôtel récupéré, on fait appel à une fonction
chambres_libres(date_arrivee, date_depart, num_hotel) qui renvoie les numéros des
chambres disponibles dans un intervalle de temps donné et dans un hôtel donné. Cette liste de
numéros de chambres est stockée dans le curseur ch_libres. On itère alors sur ce curseur, jusqu'à
ce qu'il n'y ait plus de chambre libre, ou jusqu'à ce qu'une chambre libre et de type adéquat ait été
trouvée (dans ce dernier cas, la variable OK prend pour valeur l'identifiant de la réservation
nouvellement effectuée). Dans cette itération, on récupère le type de la chambre courante : s'il
correspond au type de chambre désiré par le client (en paramètre de la fonction), on crée
effectivement la réservation et on récupère son identifiant ; sinon, on passe à la chambre libre
suivante dans le curseur. A la fin, on retourne la valeur de la variable OK qui vaut -1 si aucune
réservation n'a été faite, ou le numéro de la réservation si elle a été possible.
Notons que dans cette fonction, nous ne testons rien sur la cohérence des paramètres et de la
base, sauf le fait que l'hôtel demandé existe bien. Nous pourrions vérifier que le client existe
vraiment, que la date d'arrivée est antérieure à la date de départ, etc. Cela devrait d'ailleurs être
fait dans une programmation parfaite.
Le problème de gestion du numéro de réservation est le même que celui des numéros de facture,
mais il ne peut être gérer aussi facilement que dans la question 6. On se contente ici de faire
appelle à une mystérieuse fonction fonction_mystère() ;-)

CREATE OR REPLACE FUNCTION faire_resa (datea DATE, dated DATE, nclient NUMBER,
typch VARCHAR, villeh VARCHAR, nomh VARCHAR)
RETURN NUMBER IS
lhotel NUMBER; -- Le numéro de l'hôtel où faire la réservation
nchambre NUMBER; -- numéro d'une chambre libre
OK NUMBER := -1; -- Test de sortie de boucle + n° de résa
-- Curseur récupérant toutes les chambres libres entre les dates da et dd, dans l'hôtel identifié par nh.
CURSOR ch_libres( da DATE, dd DATE, nh NUMBER) IS
SELECT * FROM chambres_libres(da, dd, nh);
BEGIN
-- Recherche de l'hôtel exact concerné
SELECT numhotel INTO lhotel FROM hotel
WHERE ville=villeh AND nom = nomh;
IF (lhotel IS NOT NULL ) -- Nous ne faisons la tentative de réservation que si l'hôtel existe
THEN
-- Recherche d'une chambre libre pendant la période donnée
OPEN ch_libres(datea, dated, lhotel);
FETCH ch_libres INTO nchambre; -- nchambre : numéro de la chambre étudiée
-- tant que l'on n'a pas trouvé de chambre du bon type, on itère
WHILE ( ch_libres%FOUND AND OK = -1 ) LOOP -- Pour chaque chambre dispo
DECLARE
le_type VARCHAR;
BEGIN
SELECT type INTO le_type FROM chambre WHERE numchambre = nchambre ;
-- On observe le type de chambre
IF ( le_type IS NOT NULL AND le_type = typch )
THEN -- La chambre récupérée est du bon type
-- On crée la réservation
INSERT INTO reservation
(numclient, numchambre, numhotel, datearrivee, datedepart)
VALUES ( nclient, nchambre, lhotel, datea, dated);
-- On récupère le numéro de la nouvelle réservation
OK := fonction_mystère();
ELSE -- la chambre n'est pas du bon type, on passe à la suivante
FETCH ch_libres INTO nchambre;
END IF;
END;
END LOOP;
END IF;
COMMIT;
-- Retour du numéro de la réservation créée, ou -1 si aucune chambre dispo
RETURN OK;
END;

Annexe
Fonction chambres_libres() :

TYPE tableau_int IS TABLE OF NUMBER INDEX BY NUMBER; -- création d'un nouveau type : tableau de nombre

CREATE OR REPLACE FUNCTION chambres_libres (fdate DATE, tdate DATE, nhotel NUMBER)
RETURNS tableau_int IS
une_chambre NUMBER;
resultat tableau_int; -- tableau résultat
i NUMBER := 0; -- index du tableau
-- définition d'un curseur avec des paramètres
CURSOR chambres_libres(fd DATE, td DATE, nh NUMBER) IS
( SELECT chambre.numchambre
FROM ( SELECT hotel.numhotel
FROM hotel
WHERE hotel.numhotel = nh
AND NOT ((conge_debut, conge_fin) OVERLAPS (fd, td))
) hostel
JOIN chambre ON hostel.numhotel = chambre.numhotel )
MINUS
(
( SELECT chambre.numchambre
FROM chambre
JOIN ( SELECT occupation.numhotel, occupation.numchambre
FROM occupation
WHERE ( datearrivee >= fd
AND datearrivee < td )
OR ( datedepart > fd AND datedepart <= td )
OR ( ( datedepart >= td OR datedepart IS NULL )
AND datearrivee <= fd ) )
) occupees
ON ( nh = occupees.numhotel
AND chambre.numchambre = occupees.numchambre )
WHERE chambre.numhotel = nh
)
UNION
( SELECT chambre.numchambre
FROM chambre
JOIN ( SELECT reservation.numhotel, reservation.numchambre
FROM reservation
WHERE ( datearrivee >= fd AND datearrivee < td )
OR ( datedepart > fd AND datedepart <= td )
OR ( ( datedepart >= td OR datedepart IS NULL )
AND datearrivee <= fd ) )
) reservees
ON ( nh = reservees.numhotel
AND chambre.numchambre = reservees.numchambre )
WHERE chambre.numhotel = nh ) );
BEGIN
OPEN chambres_libres(fdate, tdate, nhotel);
FETCH chambres_libres INTO une_chambre;
WHILE chambres_libres%FOUND LOOP
resultat(i):=une_chambre; -- Récupérer chaque chambre calculée libre
i:= i+1;
FETCH chambres_libres INTO une_chambre;
END LOOP;
CLOSE chambres_libres;
RETURN resultat;
END;