Université de Nice Sophia-Antipolis

Cours Modèles pour la persistance des objets 2009-10

Contrôle session 1 de novembre 2009

Durée : 2h.

Seuls documents autorisés : photocopies des transparents distribués en cours ; en particulier, les énoncés et corrections des TP sont interdits. Éteignez les téléphones portables.

Important : la présentation et la lisibilité du code compteront dans la note finale. Vous êtes autorisé à écrire le code (et seulement le code) avec un crayon à papier si c'est parfaitement lisible (pas de crayon trop clair). Ajoutez des commentaires quand vous pensez que ça peut être utile au correcteur. Ne mettez pas de commentaires évidents qui n'ajoutent rien à votre code.

Respectez le découpage en questions et l'ordre des questions. Les numéros des questions devront apparaître clairement sur votre feuille.

Exercice 1 (13 points)

Une application manipule des questionnaires à choix multiples (QCM).

Le questionnaire lui-même est représenté par

Une question est représentée par

Une réponse est représentée par

Des précisions importantes :

Pour fixer les idées, voici une méthode main pour tester ces classes :

  public static void main(String[] args) {
    // Construit le questionnaire
    Questionnaire questionnaire = new Questionnaire("QCM de géographie des océans");
    questionnaire.setTheme("Géographie");
    Question question = new Question("Qu'est-ce que Java ?", true);
    question.ajouterReponse("Une île de l'océan Atlantique", false);
    question.ajouterReponse("Une île de l'océan Indien", true);
    questionnaire.ajouterQuestion(question);
    question = new Question("Combien d'habitants en France ?");
    question.ajouterReponse("Moins de 20 millions", false);
    question.ajouterReponse("Entre 20 et 50 millions", false);
    question.ajouterReponse("Plus de 50 millions", true);
    questionnaire.ajouterQuestion(question);
    // Pose les questions du questionnaire
    System.out.println(questionnaire.titre());
    System.out.println();
    while (questionnaire.resteDesQuestions()) {
      // Affiche une question et lit la réponse de l'utilisateur
      questionnaire.afficherQuestionSuivante();
      // Lit la réponse de l'utilisateur au clavier
      questionnaire.lireReponseQuestion();
      System.out.println("============");
    }
    System.out.println();
    System.out.println("Votre note : " + questionnaire.getResultat());
  }
  1. Conseil : lisez les 2 questions suivantes avant de répondre à cette question.
    Ecrivez un squelette pour chacune des classes Questionnaire, Question et Reponse de telle sorte que les questionnaires puissent être sauvegardés dans une base de données relationnelle en utilisant JPA.
    On ne vous demande pas d'écrire les méthodes ou les constructeurs (attention...), sauf s'ils présentent un intérêt du point de vue de la persistance. Ecrivez donc seulement l'en-tête de la classe, la déclaration des attributs (tous les attributs que vous imaginez être dans ces classe, même ceux qui ne sont pas persistants), les constructeurs et méthodes indispensables pour JPA et évidemment les annotations liées à JPA. Vous utiliserez l'accés par champ (pas l'accès par "getter"). Vous supposerez que toutes les classes ont des identificateurs non significatifs générés automatiquement.
    Les résultats obtenus par les utilisateurs qui ont répondu aux différents questionnaires ne sont pas sauvegardés dans la base ; tenez-en compte pour les annotations JPA. Attention, si vous estimez qu'il faut un champ pour manipuler ce résultat pendant que l'utilisateur répond, il devra apparaître dans votre réponse.
  2. Ecrivez une méthode sauvegarderQuestionnaire(Questionnaire) qui sera appelée depuis la méthode main et qui permettra de sauvegarder dans la base de données le questionnaire passé en paramètre. Vous pourrez supposer l'existence d'une méthode getQuestions qui retourne les questions d'un questionnaire dans la classe Questionnaire. Les ressources liées à la persistance devront être libérées correctement à la fin de la méthode. Vous indiquerez aussi d'éventuelles modifications à la méthode main. Est-ce que vous avez bien pensé à tout dans la question précédente pour que cette méthode fonctionne ?
  3. Un programmeur écrit une méthode rechercherQuestionnaire() qui recherche tous les questionnaires du thème "Géographie".
    Il utilise la requête JPQL "select q from Questionnaire q where q.theme='Géographie'". A la fin de la méthode il ferme toutes les ressources liées à JPA (important ; en particulier le gestionnaire d'entités est fermé) et il renvoie une liste de tous les questionnaires trouvés.
    Il appelle cette méthode depuis la fin de la méthode main ; la méthode s'exécute bien et renvoie bien les questionnaires voulus mais une exception NullPointerException est lancée sur l'instruction de la méthode suivante de la classe Questionnaire (n'essayez pas de comprendre ce code, il vous suffit de comprendre ce qui provoque l'exception) :
    public boolean resteDesQuestions() {
      return numeroQuestionEnCours != questions.size() - 1;
    }
      
    Expliquez le problème. Que changer dans la méthode rechercherQuestionnaire pour que le programme s'exécute sans exception ?

Correction

1.

Classe Questionaire

/**
 * Un QCM.
 */
@Entity
public class Questionnaire {
  @Id @GeneratedValue
  private int id;
  /**
   * Les questions du QCM.
   */
  @ManyToMany
  private ArrayList<Question> questions = new ArrayList<Question>();
  /**
   * Le numéro de la dernière question posée.
   */
  @Transient
  private int numeroQuestionEnCours = -1;
  /**
   * Le titre du Questionnaire.
   */
  private String titre;
  /**
   * Le thème du questionnaire. Permet de classer les questionnaires.
   */
  private String theme;
  /**
   * Total des points gagnés par l'utilisateur
   */
  @Transient
  private double total = 0.0;
  
  // Obligatoire avec JPA
  public Questionnaire() {
  }

Classe Question :

/**
 * Une question du QCM
 */
@Entity
public class Question {
  @Id @GeneratedValue
  private int id;
  /**
   * Les questionnaires qui contiennent cette question.
   */
  @ManyToMany(mappedBy="questions")
  private List<Questionnaire> questionnaires = new ArrayList<Questionnaire>();
  /**
   * Les réponses possibles aux questions.
   * Elles sont chargées en mémoire lorsque la question l'est.
* Elles sont rendues persistantes lorsque la question l'est.
*/ @OneToMany(cascade={CascadeType.PERSIST,CascadeType.MERGE}, fetch=FetchType.EAGER) private List<Reponse> reponses = new ArrayList<Reponse>(); /** * Enoncé de la question */ private String enonce; private boolean reponsesMultiples; public Question() { }

Classe Reponse :

/**
 * Une réponse possible pour une question
 */
@Entity
public class Reponse {
  @Id @GeneratedValue
  private int id;
  
  /**
   * Intitulé de cette réponse.
   */
  private String intitule;
  /**
   * Vrai si et seulement si la réponse doit être "cochée" pour donner
   * une bonne réponse à la question.
   */
  private boolean ok;
  
  public Reponse() {
  }

2.

  private static void sauvegarderQuestionnaire(Questionnaire q) {
    EntityManagerFactory emf = null;
    EntityManager em = null;
    EntityTransaction tx = null;
    try {
      emf = Persistence.createEntityManagerFactory("QCM");
      em = emf.createEntityManager();
      tx = em.getTransaction();
      tx.begin();
      List questions = q.getQuestion();
      for (Question question : questions) {
        em.persist(question);
      }
      em.persist(q);
      tx.commit();
    }
    catch(Exception e) {
      if (tx != null) {
        tx.rollback();
      }
    }
    finally {
      if (em != null) {
        em.close();
      }
      if (emf != null) {
        emf.close();
      }
    }
  }

3. Par défaut les associations ManyToMany sont chargées en mode lazy. Donc les questions ne sont pas chargées en mémoire lorsque la requête JPQL est exécutée. Comme le gestionnaire d'entités est fermé, il ne peut plus récupérer les questions lorsque le questionnaire fait appel à elle. La liste questions est null et c'est elle qui provoque la NullPointerException.

Il faut changer cette requête pour charger aussi les questions en mémoire :

select q from Questionnaire q join fetch q.questions where q.theme='Géographie'

Remarque : les réponses sont chargées en mémoire automatiquement lorsque les questions le sont à cause du fetch=FetchType.EAGER de l'association entre Question et Reponse. Sans cet attribut, il y aurait de même une NullPointerException lorsqu'un accès aux réponses aurait lieu.

Exercice 2 (4 points)

  1. Ecrivez les instruction SQL pour créer la ou les tables qui contiendront les données de la classe Question et ses liaisons avec les tables Reponse et Questionnaire (on ne vous demande pas les tables correspondant aux classes Reponse et Questionnaire). N'oubliez pas les contraintes d'intégrité.
      create table question(
      id integer primary key,
      enonce varchar(100),
      reponses_multiples char(1) constraint ck_reponses_multiples reponses_multiples in ('o', 'n'))
      
      create table questionnaire_question(
      id_questionnaire integer references questionnaire,
      id_question integer references question),
      primary key(id_questionnaire, id_question)
      
  2. Si vous aviez utilisé un SGBD objet-relationnel, quelle autre choix auriez-vous pu avoir pour la structure de cette table ? On ne vous demande pas de code SQL mais vous pouvez en donner si vous voulez.
    Les réponses pourraient être mises dans la table des questions sous la forme d'une liste (ou d'un varray pour Oracle). En ce cas il n'y aurait pas de table reponse.

Exercice 3 (3 points)

On veut écrire une classe DAO simple qui utilise JDBC pour gérer la persistance de la classe Reponse. On n'utilise pas le modèle "fabrique abstraite" ni des exceptions indépendantes de SQL.

  1. Ecrivez les en-têtes des méthodes pour retrouver une réponse dont on connaît l'identificateur (le R de CRUD), pour modifier une réponse dont on connaît l'identificateur (le U de CRUD).
  2. Même question pour la méthode qui enregistre une nouvelle réponse dans la base de données (le C de CRUD) ? N'y a-t-il pas un problème ? Comment pourrait-on faire ?

Correction

  1. public Reponse findById(int id) throws SQLException
    public void update(Reponse reponse) throws SQLException
  2. Le problème est qu'on ne peut donner l'en-tête
    public void create(Reponse reponse) throws SQLException
    car on ne pourrait ranger dans la base l'association entre la réponse et la question à laquelle elle est liée car cette association est unidirectionnelle et la réponse ne connaît pas sa question.
    Plusieurs solutions possibles. La plus simple est de passer la question en paramètre de la méthode create :
    public void create(Reponse, reponse, Question question) throws SQLException
    Une autre solution serait d'ajouter dans la base des réponses dans le DAO des questions. Mais il faudrait alors que toutes les réponses à une question soient créées en mémoire centrale au moment de l'ajout dans la base.