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 , numcham bre, numh otel, datearrivee, datedepart) reservation(numresa, numcli ent, numc hambre , 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; BEGIN SELECT etoile INTO resultat FROM hotel WHERE nom = nom_hotel AND ville = ville_hotel; RETURN resultat; END;

-- déclaration de la variable

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_fact ure, date _facture , num_occu pation , mont ant_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; une_occup occupation%ROWTYPE; prix_sejour NUMBER(4); la_facture NUMBER; BEGIN

-- le prix de la chambre qui a été occupée -- l'occupation en cours de traitement (1 ligne) -- le prix total du séjour -- numéro de la facture de l'occupation n° nocc

-- 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 ) 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;

-- On crée la facture que si elle n'existe pas déjà

RETURN nfact ; END;

-- Retour du numéro de la facture créée ou récupérée

CREATE OR REPLACE FUNCTION creations_factures() RETURN NUMBER IS

une_occup occupation%ROWTYPE; CURSOR les_occupations IS SELECT * FROM occupation; nbfact NUMBER := 0; BEGIN OPEN les_occupations; FETCH les_occupations INTO une_occup; WHILE les_occupations%FOUND LOOP

-- Les données de l'occupation en cours de traitement -- Toutes les occupations -- Le nombre de factures créées

-- Récupération de toutes les occupations -- Début de parcours des occupations

-- 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; END LOOP; CLOSE les_occupations; COMMIT; RETURN nbfact; END;

-- On passe à l'occupation suivante

-- Fermeture des occupations: il n'y en a plus -- modification effective de la base

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;

une_occup

un_client client%ROWTYPE;

un_hotel hotel%ROWTYPE;

numch chambre.numchambre%TYPE;

text VARCHAR;

occupation%ROWTYPE;

-- La facture à éditer -- L'occupation concernée -- Le client concerné -- L'hôtel concerné -- Le numéro de la chambre -- 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;

END;

-- Retour du texte édité

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; nchambre NUMBER; OK NUMBER := -1;

-- Le numéro de l'hôtel où faire la réservation -- numéro d'une chambre libre -- 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

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;