Entités JPA avec Spring

Transformer les colonnes de la base en objets Java

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Les entités sont un composant important de l'interface Java Persistence. Elles sont gérées par Entity Manager et facilitent la manipulation des tables de la base de données. A travers cet article on verra l'aspect un peu plus pragmatique des entités.

Entités en JPA avec Spring

Comme on l'a évoqué plus haut, une entité représente la table d'une base de données dans l'application. Elle ressemble à un JavaBean avec des caractéristiques propres à des entités. Voici les propriétés que devrait respecter chaque entité :
- doit être annotée avec javax.persistence.Entity
- doit posséder un constructeur publique ou protégé ne prenant pas des arguments
- ni elle, ni ses attributs ne peuvent pas être déclarés finaux
- doit implémenter l'interface Serializable car parfois elle doit être transmise dans une autre représentation (par exemple pour être stockée dans la session HTTP)
- peut hériter d'une classe n'étant pas une entité; une classe non-entité peut également hériter d'une entité
- ses attributs peuvent être accessibles et manipulés uniquement à travers des getters et des setters; ils doivent donc être soit privés, soit protégés

Voici l'entité Lang, représentant la table lang, avec quelques fragments les plus intéressants :

@Entity
@Table(name="lang")
public class Lang implements Serializable {
    private Long id;
    private String iso;
    private String name;
    private List<Question> questions;
    // other fields ommitted

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name="id_la")
    public Long getId() {
        return id;
    }
    @Column(name="name_la")
    public String getName() {
        return name;
    }
    @OneToMany(mappedBy = "lang")
    public List<Question> getQuestions() {
        return questions;
    }
    // ... other getters and all setters are ommitted
}

Découpons ce fichier en sous-parties pour mieux le comprendre :

  1. @Entity : marque la classe en tant qu'une entité
  2. @Table(name = "lang") : spécifie la table que représente l'entité
  3. @Id : marque un champ (ou un getter) en tant qu'une clé primaire
  4. @GeneratedValue(strategy = IDENTITY) : fournit la stratégie de génération de la clé primaire. Les stratégies suivantes sont disponibles :
    - AUTO : le fournisseur de persistence décide quelle stratégie appliquer (la valeur par défaut)
    - IDENTITY : génère une nouvelle valeur pour chaque nouvelle entité commitée. Les valeurs générées sont uniques
    - SEQUENCE : la clé est générée avant le commit de l'entité dans la base. La valeur est générée par un générateur, spécifié dans l'attribut generator
    - TABLE : la génération de clé est basée sur une table à chaque fois quand on insère une nouvelle ligne. Si on l'utilise avec Hibernate, la table contenant des séquences s'appelle hibernate_sequences. Elle possède deux colonnes : une avec le nom de l'entité, l'autre avec le plus grand identifiant généré à cette entité. En se basant sur ces deux valeurs, il génère la clé correspondant
  5. @Column(name="id_la") : indique la colonne dans la base de données à laquelle correspond l'attribut (ou son getter)
  6. @OneToMany(mappedBy = "lang") : représente une relation one-to-many où l'entité possède plusieurs éléments dans une autre table. En occurrence, une langue possède plusieurs questions. L'attribut mappedBy signifie le nom de l'attribut qui gère la relation. Pour le constater, voici un fragment de l'entité Question :
    // ... still without imports
    @Entity
    @Table(name="question")
    public class Question implements Serializable {
        // ...
        private Lang lang;
        @ManyToOne(fetch = FetchType.LAZY)
        @JoinColumn(name = "lang_id_la")
        public Lang getLang() {
            return lang;
        }
        // ...
    }
    
    L'annotation @JoinColumn signale la colonne avec la clé étrangère. Dans notre cas, la colonne lang_id_la de la table question réfère à la table lang. L'annotation @ManyToOne est une opposition à @OneToMany. Elle signifie que l'entité Question récupère à chaque fois une seule instance de l'entité Lang. L'attribut fetch de cette annotation signifie la manière selon laquelle on doit charger l'entité. Des valeurs possibles sont :
    - LAZY : on charge l'entité associée seulement quand elle est accédée pour la première fois (chargement à la demande)
    - EAGER : on charge l'entité associée tout de suite, avec le reste d'éléments. Cette solution peut poser des soucis de performance dès lors qu'on a un grand amont des données à charger

Une nouvelle entité, celle représentant l'emprunt de l'exemplaire d'un livre, introduit quelques nouveaux concepts : clé primaire composée de plusieurs colonnes et une colonne stockant des données liées au temps. Voici les fragments qui nous intéressent :

// imports ommitted
@Entity
@Table(name="booking")
public class Booking implements Serializable {

    private BookingPK bookingPK;
    private Date bookingDate;
    
    @EmbeddedId
    @AttributeOverrides({
        @AttributeOverride(name = "idBookCopy", column = @Column(name = "book_copy_id_bc", nullable = false)),
        @AttributeOverride(name = "idSubscriber", column = @Column(name = "subscriber_id_su", nullable = false))
    })
    public BookingPK getBookingPK() {
        return bookingPK;
    }
    @Temporal(DATE)
    @Column(name = "date_boo")
    public Date getBookingDate() {
        return bookingDate;
    }
    // the rest of the code is ommitted
}

Et voici la classe de notre clé primaire bookingPK :

// still without imports
public class BookingPK implements Serializable {
    private long idBookCopy;
    private long idSubscriber;

    public BookingPK() {}

    public long getIdBookCopy() {
	    return idBookCopy;
    }
    public long getIdSubscriber() {
	    return idSubscriber;
    }
    // setters, hashCode and equals here
}

Pour définir une clé primaire composée sous JPA, on doit créer une nouvelle classe regroupant des colonnes qui constituent cette clé. En occurrence, il s'agit du BookingPK qui définit la clé avec des colonnes book_copy_id_bc et subscriber_id_su. Cette classe n'a rien de compliqué. Ce qui ne doit pas être forcément le cas de la classe Booking.

On commencera son analyse par l'annotation @Temporal. Elle spécifie les champs qui contiennent les instances des classes java.util.Date ou java.util.Calendar (ou des classes qui les héritent). Dans ce cas, on utilise l'instance de cette première classe. C'est pourquoi on précise son type à Date. Liste des types disponibles se trouve dans la JavaDoc de l'EnumType.

Passons maintenant à la clé primaire composée. Tout d'abord, on remarque l'annotation @EmbeddedId. Elle indique que l'attribut concernée est une clé primaire composée et que cette clé est une nouvelle classe. L'annotation @AttributeOverrides liste des attributs de cette nouvelle classe et les fait correspondre au format de la clé primaire dans la table.

Dans certaines situations, comme par exemple celle où l'on a besoin d'utiliser un entité dans un formulaire, on peut devoir utiliser des attributs ne faisant pas partie de la table dans la base. Pour signaler cela au Spring, il faut utiliser l'annotation @Transient, comme dans l'exemple ci-dessous :

@Entity
@Table(name = "writer_lang")
public class WriterLang  implements Serializable {
    @Transient
    public String getType() {
        return type;
    }
    // other code is ommitted
}
Bartosz KONIECZNY Couche des données

Une question ? Une remarque ?

*

*

Un conseil Symfony1

Comment détecter si l'utilisateur est connecté ?

Dans notre fichier layout.php on contient une partie réservée aux utilisateurs connectés et non-connectés. Pour pouvoir détecter cela sans trop de complication, on devrait utiliser les slots. Le fichier d'action va contenir le framgent suivant en fonction du type de l'utilisateur (connecté ou pas) :

$this->getResponse()->setSlot('isConnected', 'true');
Alors dans le fichier layout.php on pourra utiliser ceci :
if(!has_slot('isConnected')  ) {
  // include login form
  include_partial('users/formLogin');
}
else {
  include_partial('users/accountBox');
}