Participer au site avec un Tip
Rechercher
 

Améliorations / Corrections

Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.

Emplacement :

Description des améliorations :

Mapping d'une relation d'héritage

Mapping des collections Exemple d'un mapping complet



Accès rapide :
La vidéo
Introduction au mapping d'une relation d'héritage
Stratégie « une table pour la hiérarchie de classes »
Mise en oeuvre de la base de données
Le mapping associé à cette stratégie
Test de notre mapping
Stratégie « tables jointes »
Mise en oeuvre de la base de données
Le mapping associé à cette stratégie
Test de notre mapping
Stratégie « une table par classes concrète »
Mise en oeuvre de la base de données
Le mapping associé à cette stratégie
Test de notre mapping

La vidéo

Cette vidéo vous montre comment mapper vos relations d'héritage en base de données. Les trois stratégies possibles sont présentées une à une.


Mapping JPA d'une relation d'héritage

Introduction au mapping d'une relation d'héritage

JPA est une spécification d'ORM pour Java. Comme nous l'avons déjà vu, l'acronyme ORM signifie Object Relational Mapping. Un tel système cherche à faire correspondre la vision « base de données » (persistance) avec la vision objet. Or, la POO (Programmation Orientée Objet) propose un mécanisme fondamental : l'héritage. Qu'en est-il des possibilités de prise en charge de l'héritage avec JPA ?

Pas de panique. JPA support parfaitement l'héritage. Mieux encore vous avez trois stratégies distinctes pour stocker les objets d'une hiérarchie de classes Java en base de données. La différence entre ces trois stratégies réside dans la manière dont les données vont être stockées en mémoire. C'est trois stratégies sont :

Nous allons tester ces trois possibilités une par une. Pour ce faire nous allons considérer la hiérarchie de classes proposée dans le diagramme UML qui suit. Ces classes vont permettre de représenter les paiements pour notre site de vente en ligne. On peut considérer deux types distincts de paiements : les paiements PayPal et les paiements par cartes bleues. Les deux classes associées à ces deux types de paiements dériveront d'une classe mère permettant de factoriser tous les attributs communs à tous les types de paiements.

Diagramme UML d'une relation d'héritage

Stratégie « une table pour la hiérarchie de classes »

Mise en oeuvre de la base de données

Donc, avec cette stratégie, vous n'avez besoin que d'une seule table. Nous allons l'appeler T_Payments. Pour distinguer le type de paiement, nous allons avoir une colonne de plus que le nombre d'attributs proposé ci-dessus : cette colonne sera appelée le discriminant et en fonction de sa valeur nous aurons à faire à un type de paiement ou à un autre. Le discriminant sera, en base de données, ce qu'il y aura de plus proche de notre notion d'héritage.

Voici le code SQL à exécuter en base pour construire notre base de données de test.

-- ------------------------------------------------------------------------------
-- - Reconstruction de la base de données                                     ---
-- ------------------------------------------------------------------------------

DROP DATABASE IF EXISTS Inheritance;
CREATE DATABASE Inheritance;
USE Inheritance;

-- -----------------------------------------------------------------------------
-- - Construction de la table des paiements                                  ---
-- -----------------------------------------------------------------------------

CREATE TABLE T_Payments (
    idPayment           int          PRIMARY KEY AUTO_INCREMENT,
    amount              double       NOT NULL,
    paymentDate         datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    discriminator       tinyint      NOT NULL,
    accountNumber       varchar(30),
    cardNumber          char(24),
    expirationDate      varchar(5)
);

INSERT INTO T_Payments (amount, paymentDate, discriminator, accountNumber, cardNumber, expirationDate) 
VALUES ( 1295, now(), 1, 'A fake paypal account', NULL, NULL ),
       ( 245, now(), 2, NULL, '1234 5678 9012 3456', '06/19' );

SELECT * FROM T_Payments;
Fichier Database.sql

Dans notre exemple, la valeur de discriminant 1 correspondra à un paiement Paypal alors qu'une valeur de 2 correspondra à un paiement par carte bleue. En fonction du fait que vous autorisiez (ou non) un paiement sans plus de précisions, (associé à une instance de la classe Payment) vous pouvez (ou non) considérer une valeur pour ce type de paiement (pourquoi pas 0).

Pour construire la base de données, veuillez de nouveau exécuter l'ordre source Database.sql dans la console de MariaDB. Voici les résultats produits.

MariaDB [(none)]> source Database.sql
Query OK, 1 row affected (0.069 sec)

Query OK, 1 row affected (0.000 sec)

Database changed
Query OK, 0 rows affected (0.032 sec)

Query OK, 2 rows affected (0.006 sec)
Records: 2  Duplicates: 0  Warnings: 0

+-----------+--------+---------------------+---------------+-----------------------+---------------------+----------------+
| idPayment | amount | paymentDate         | discriminator | accountNumber         | cardNumber          | expirationDate |
+-----------+--------+---------------------+---------------+-----------------------+---------------------+----------------+
|         1 |   1295 | 2020-01-25 10:43:24 |             1 | A fake paypal account | NULL                | NULL           |
|         2 |    245 | 2020-01-25 10:43:24 |             2 | NULL                  | 1234 5678 9012 3456 | 06/19          |
+-----------+--------+---------------------+---------------+-----------------------+---------------------+----------------+
2 rows in set (0.000 sec)
MariaDB [Inheritance]> 

Si l'on doit trouver un avantage à cette stratégie de mapping de l'héritage, on peut signaler qu'aucune jointure n'est nécessaire pour retrouver toutes les données d'un paiement : cela pour être relativement rapide. Par contre, on consomme plus d'espace de stockage que nécessaire : plus vous aurez un grand nombre de classes dérivées et plus vous aurez de la perte sèche représentée par les valeurs NULL dans l'affichage ci-dessus. Cela vient du fait que tous les attributs possibles pour toutes les classes de la hiérarchie doivent apparaître sous forme de colonnes dans cette table.

Le mapping associé à cette stratégie

Pour introduire une relation d'héritage dans un mapping JPA il faut utiliser l'annotation @Inheritance. Cette annotation permet notamment de fixer la stratégie de mapping à utiliser : dans notre cas, il s'agit de la stratégie @Inheritance( strategy = InheritanceType.SINGLE_TABLE ). Il faut aussi spécifier qu'elle est la colonne qui sert de discriminant grâce à l'annotation @DiscriminatorColumn.

Voici, de manière synthétique, l'ensemble des annotations JPA à porter sur nos différentes classes.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
@Entity
@Table( name = "T_Payments" )
@Inheritance( strategy = InheritanceType.SINGLE_TABLE )
@DiscriminatorColumn( name="discriminator", discriminatorType = DiscriminatorType.INTEGER )
//@DiscriminatorValue("0")      // Si la classe Payment n'était pas abstraite
public abstract class Payment {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int idPayment = 0;
    
    // Suite de la classe ...

}

@Entity 
@DiscriminatorValue("1")
public class PaypalPayment extends Payment {
    
    // Suite de la classe ...

}

@Entity 
@DiscriminatorValue("2")
public class CreditCardPayment extends Payment {
    
    // Suite de la classe ...

}
Extraits de code représentant les différentes annotations JPA à utiliser
les annotations @DiscriminatorValue permettent d'associer chaque valeur de discriminant à une classe de la hiérarchie. La classe de base peut avoir sa propre valeur de discriminant à condition qu'elle ne soit pas abstraite (qu'on puisse en manipuler des instances).
bien que notre colonne de discriminant soit d'un type numérique, il est nécessaire de passer la valeur à l'annotation @DiscriminatorValue sous forme d'une chaîne de caractères.
on trouve une seule fois l'annotation @Table étant donné qu'il n'existe qu'une seule et unique table.

Et voici maintenant le code complet (avec les imports requis) de la classe Payment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
package fr.koor.webstore.business;

import java.util.Date;

import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table( name = "T_Payments" )
@Inheritance( strategy = InheritanceType.SINGLE_TABLE )
@DiscriminatorColumn( name="discriminator", discriminatorType = DiscriminatorType.INTEGER )
//@DiscriminatorValue("0")      // Si la classe Payment n'était pas abstraite
public abstract class Payment {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int idPayment = 0;
    private double amount = 0;
    private Date paymentDate = new Date();

    public Payment() {
    }

    public int getIdPayment() {
        return idPayment;
    }

    public void setIdPayment(int idPayment) {
        this.idPayment = idPayment;
    }

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }

    public Date getPaymentDate() {
        return paymentDate;
    }

    public void setPaymentDate(Date paymentDate) {
        this.paymentDate = paymentDate;
    }

    @Override
    public String toString() {
        return "Payment: idPayment=" + idPayment + ", amount=" + 
               amount + ", paymentDate=" + paymentDate;
    }
    
}
La classe Payment et son mapping JPA

Voici à présent le code complet de la classe PaypalPayment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
package fr.koor.webstore.business;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity 
@DiscriminatorValue("1")
public class PaypalPayment extends Payment {

    private String accountNumber = "unknown";
    
    public PaypalPayment() {
        super();
    }
    
    public String getAccountNumber() {
        return accountNumber;
    }
    
    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }
    
    @Override
    public String toString() {
        return super.toString() + ", accountNumber=" + accountNumber;
    }
}
La classe PaypalPayment et son mapping JPA

Et voici le code complet de la classe CreditCardPayment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
package fr.koor.webstore.business;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity 
@DiscriminatorValue("2")
public class CreditCardPayment extends Payment {

    private String cardNumber = "unknown";
    private String expirationDate = "mm/yy";
    
    public CreditCardPayment() {
        super();
    }
    
    public String getCardNumber() {
        return cardNumber;
    }
    
    public void setCardNumber(String cardNumber) {
        this.cardNumber = cardNumber;
    }
    
    public String getExpirationDate() {
        return expirationDate;
    }
    
    public void setExpirationDate(String expirationDate) {
        this.expirationDate = expirationDate;
    }
    
    @Override
    public String toString() {
        return super.toString() + ", cardNumber=" + cardNumber + ", expirationDate=" + expirationDate;
    }
}
La classe CreditCardPayment et son mapping JPA

Test de notre mapping

Nous allons maintenant procéder à un test pour vérifier que notre mapping fonctionne correctement. Pour ce faire, il faut configurer l'unité de persistance de notre projet pour qu'elle pointe bien sur notre nouvelle base de données. Voici le contenu du fichier /META-INF/persistence.xml.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence 
                   http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
             version="2.0">
             
    <persistence-unit name="WebStore">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        
        <class>fr.koor.webstore.business.Payment</class> 
        <class>fr.koor.webstore.business.CreditCardPayment</class> 
        <class>fr.koor.webstore.business.PaypalPayment</class> 
        
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:mariadb://localhost/Inheritance" />
            <property name="javax.persistence.jdbc.user" value="root" />
            <property name="javax.persistence.jdbc.password" value="" />
            
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" />
            <property name="hibernate.format_sql" value="false" />
        </properties>
    </persistence-unit>
    
</persistence>
Le fichier de configuration JPA/Hibernate : /META-INF/persistence.xml

Et voici un exemple de code permettant de charger toutes instances de paiements, ce quel que soit le type de paiement utilisé. Vous noterez que l'exemple de code cherche à afficher le type de la classe utilisée pour chaque paiement.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
package fr.koor.webstore;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.persistence.TypedQuery;

import fr.koor.webstore.business.Payment;

public class Console {

    public static void main(String[] args) throws Exception {
        EntityManagerFactory entityManagerFactory = null;
        EntityManager entityManager = null;
        
        try {
            
            entityManagerFactory = Persistence.createEntityManagerFactory("WebStore");
            entityManager = entityManagerFactory.createEntityManager();
        
            TypedQuery<Payment> query = entityManager.createQuery( "from Payment", Payment.class );
            List<Payment> payments = query.getResultList();
            for (Payment payment : payments) {
                System.out.println( payment.getClass().getName() );
                System.out.println( "\t" + payment );
            }
            

        } finally {
            if ( entityManager != null ) entityManager.close();
            if ( entityManagerFactory != null ) entityManagerFactory.close();
        }
    }
}
Exemple de manipulation de nos paiements.

Si vous lancer ce programme, vous devriez obtenir les résultats suivants.

[main] WARN  | org.hibernate.orm.connections.pooling                        | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70)
fr.koor.webstore.business.PaypalPayment
    Payment: idPayment=1, amount=1295.0, paymentDate=2020-01-25 10:43:24.0, accountNumber=A fake paypal account
fr.koor.webstore.business.CreditCardPayment
    Payment: idPayment=2, amount=245.0, paymentDate=2020-01-25 10:43:24.0, cardNumber=1234 5678 9012 3456, expirationDate=06/19

Stratégie « tables jointes »

Passons à la seconde stratégie dites par « tables jointes ». Il s'agit certainement de la stratégie la plus couramment mise en oeuvre. Pour faire simple, il va falloir définir une table par classe et chaque lien l'héritage donnera lieu à une relation entre les deux tables correspondantes.

Mise en oeuvre de la base de données

Voici le nouveau script SQL permettant de définir chacune des tables nécessaires.

-- ------------------------------------------------------------------------------
-- - Reconstruction de la base de données                                     ---
-- ------------------------------------------------------------------------------

DROP DATABASE IF EXISTS Inheritance;
CREATE DATABASE Inheritance;
USE Inheritance;

-- -----------------------------------------------------------------------------
-- - Construction de la table des paiements                                  ---
-- -----------------------------------------------------------------------------

CREATE TABLE T_Payments (
    idPayment           int          PRIMARY KEY AUTO_INCREMENT,
    amount              double       NOT NULL,
    paymentDate         datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO T_Payments (amount, paymentDate) 
VALUES ( 1295, now() ),
       ( 245, now() );

SELECT * FROM T_Payments;

-- -----------------------------------------------------------------------------
-- - Construction de la table des paiements paypal                           ---
-- -----------------------------------------------------------------------------

CREATE TABLE T_PaypalPayments (
    idPayment           int          NOT NULL REFERENCES T_Payments(idPayment),
    accountNumber       varchar(30)
);

INSERT INTO T_PaypalPayments VALUES ( 1, 'A fake paypal account' );

SELECT * FROM T_PaypalPayments;

-- -----------------------------------------------------------------------------
-- - Construction de la table des paiements par cartes bleues                ---
-- -----------------------------------------------------------------------------

CREATE TABLE T_CreditCardPayments (
    idPayment           int          NOT NULL REFERENCES T_Payments(idPayment),
    cardNumber          char(24),
    expirationDate      varchar(5)
);

INSERT INTO T_CreditCardPayments VALUES ( 2, '1234 5678 9012 3456', '06/19' );

SELECT * FROM T_CreditCardPayments;
Fichier Database.sql

Pour construire la base de données, veuillez de nouveau exécuter l'ordre source Database.sql dans la console de MariaDB. Voici les résultats produits.

MariaDB [(none)]> source Database.sql
Query OK, 1 row affected (0.062 sec)

Query OK, 1 row affected (0.000 sec)

Database changed
Query OK, 0 rows affected (0.011 sec)

Query OK, 2 rows affected (0.002 sec)
Records: 2  Duplicates: 0  Warnings: 0

+-----------+--------+---------------------+
| idPayment | amount | paymentDate         |
+-----------+--------+---------------------+
|         1 |   1295 | 2020-01-25 15:01:18 |
|         2 |    245 | 2020-01-25 15:01:18 |
+-----------+--------+---------------------+
2 rows in set (0.000 sec)

Query OK, 0 rows affected (0.012 sec)

Query OK, 1 row affected (0.002 sec)

+-----------+-----------------------+
| idPayment | accountNumber         |
+-----------+-----------------------+
|         1 | A fake paypal account |
+-----------+-----------------------+
1 row in set (0.000 sec)

Query OK, 0 rows affected (0.028 sec)

Query OK, 1 row affected (0.006 sec)

+-----------+---------------------+----------------+
| idPayment | cardNumber          | expirationDate |
+-----------+---------------------+----------------+
|         2 | 1234 5678 9012 3456 | 06/19          |
+-----------+---------------------+----------------+
1 row in set (0.000 sec)

MariaDB [Inheritance]>

Le mapping associé à cette stratégie

Nous allons continuer à utiliser l'annotation @Inheritance, mais cette fois-ci la stratégie sera @Inheritance( strategy = InheritanceType.JOINED ). Comme nous avons plusieurs tables en base de données, il faudra aussi associer chaque classe à sa table et, bien entendu, définir les colonnes de mise en relation.

Voici, de manière synthétique, l'ensemble des annotations JPA à porter sur nos différentes classes pour exploiter cette nouvelle stratégie.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
@Entity
@Table( name = "T_Payments" )
@Inheritance( strategy = InheritanceType.JOINED )
public abstract class Payment {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int idPayment = 0;
    
    // Suite de la classe ...

}

@Entity 
@Table( name = "T_PaypalPayments" )
@PrimaryKeyJoinColumn( name = "idPayment" )
public class PaypalPayment extends Payment {
    
    // Suite de la classe ...

}

@Entity 
@Table( name = "T_CreditCardPayments" )
@PrimaryKeyJoinColumn( name = "idPayment" )
public class CreditCardPayment extends Payment {
    
    // Suite de la classe ...

}
Extraits de code représentant les différentes annotations JPA à utiliser

Et voici maintenant le code complet (avec les imports requis) de la classe Payment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
package fr.koor.webstore.business;

import java.util.Date;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table( name = "T_Payments" )
@Inheritance( strategy = InheritanceType.JOINED )
public abstract class Payment {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int idPayment = 0;
    private double amount = 0;
    private Date paymentDate = new Date();

    public Payment() {
    }

    public int getIdPayment() {
        return idPayment;
    }

    public void setIdPayment(int idPayment) {
        this.idPayment = idPayment;
    }

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }

    public Date getPaymentDate() {
        return paymentDate;
    }

    public void setPaymentDate(Date paymentDate) {
        this.paymentDate = paymentDate;
    }

    @Override
    public String toString() {
        return "Payment: idPayment=" + idPayment + ", amount=" + 
               amount + ", paymentDate=" + paymentDate;
    }
    
}
La classe Payment et son mapping JPA

Voici à présent le code complet de la classe PaypalPayment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
package fr.koor.webstore.business;

import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

@Entity 
@Table( name = "T_PaypalPayments" )
@PrimaryKeyJoinColumn( name = "idPayment" )
public class PaypalPayment extends Payment {

    private String accountNumber = "unknown";
    
    public PaypalPayment() {
        super();
    }
    
    public String getAccountNumber() {
        return accountNumber;
    }
    
    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }
    
    @Override
    public String toString() {
        return super.toString() + ", accountNumber=" + accountNumber;
    }
}
La classe PaypalPayment et son mapping JPA

Et voici le code complet de la classe CreditCardPayment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
package fr.koor.webstore.business;

import javax.persistence.Entity;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.Table;

@Entity 
@Table( name = "T_CreditCardPayments" )
@PrimaryKeyJoinColumn( name = "idPayment" )
public class CreditCardPayment extends Payment {

    private String cardNumber = "unknown";
    private String expirationDate = "mm/yy";
    
    public CreditCardPayment() {
        super();
    }
    
    public String getCardNumber() {
        return cardNumber;
    }
    
    public void setCardNumber(String cardNumber) {
        this.cardNumber = cardNumber;
    }
    
    public String getExpirationDate() {
        return expirationDate;
    }
    
    public void setExpirationDate(String expirationDate) {
        this.expirationDate = expirationDate;
    }
    
    @Override
    public String toString() {
        return super.toString() + ", cardNumber=" + cardNumber + ", expirationDate=" + expirationDate;
    }
}
La classe CreditCardPayment et son mapping JPA

Test de notre mapping

Pour tester votre mapping, vous pouvez reprendre le main utilisé avec la stratégie précédente : normalement, les mêmes résultats devraient être produits.

[main] WARN  | org.hibernate.orm.connections.pooling                        | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70)
fr.koor.webstore.business.PaypalPayment
    Payment: idPayment=1, amount=1295.0, paymentDate=2020-01-25 10:43:24.0, accountNumber=A fake paypal account
fr.koor.webstore.business.CreditCardPayment
    Payment: idPayment=2, amount=245.0, paymentDate=2020-01-25 10:43:24.0, cardNumber=1234 5678 9012 3456, expirationDate=06/19

Stratégie « une table par classes concrète »

On attaque la dernière stratégie : « une table par classes concrète ». L'idée est de ne pas avoir de jointures, mais aussi de ne pas avoir de perte sèche. Du coup, on va créer une table en base de données par classe concrète. Le contre coup, c'est que pour certains calculs (par exemple, total des encaissements du jour), plusieurs tables devront être requêtées pour, au final, agréger les résultats.

Mise en oeuvre de la base de données

Notez que dans notre cas, la classe Payment est abstraite : il n'y aura donc pas de table associée. Pour les deux autres tables ( et ) on y retrouvera des colonnes pour chaque attribut, y compris ceux hérités de la classe Payment. Voici le code SQL de construction de ces tables.

-- ------------------------------------------------------------------------------
-- - Reconstruction de la base de données                                     ---
-- ------------------------------------------------------------------------------

DROP DATABASE IF EXISTS Inheritance;
CREATE DATABASE Inheritance;
USE Inheritance;

-- -----------------------------------------------------------------------------
-- - Construction de la table des paiements paypal                           ---
-- -----------------------------------------------------------------------------

CREATE TABLE T_PaypalPayments (
    idPayment           int          PRIMARY KEY,
    amount              double       NOT NULL,
    paymentDate         datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    accountNumber       varchar(30)
);

INSERT INTO T_PaypalPayments VALUES ( 1, 1295, now(), 'A fake paypal account' );

SELECT * FROM T_PaypalPayments;

-- -----------------------------------------------------------------------------
-- - Construction de la table des paiements par cartes bleues                ---
-- -----------------------------------------------------------------------------

CREATE TABLE T_CreditCardPayments (
    idPayment           int          PRIMARY KEY,
    amount              float        NOT NULL,
    paymentDate         datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
    cardNumber          char(24),
    expirationDate      varchar(5)
);

INSERT INTO T_CreditCardPayments VALUES ( 2, 245, now(), '1234 5678 9012 3456', '06/19' );

SELECT * FROM T_CreditCardPayments;
Fichier Database.sql
la génération automatique des valeurs de clé primaire a été retiré. Effectivement cette possibilité est incompatible avec cette stratégie. Nous allons y revenir.

Pour construire la base de données, veuillez de nouveau exécuter l'ordre source Database.sql dans la console de MariaDB. Voici les résultats produits.

MariaDB [(none)]> source Database.sql
Query OK, 0 rows affected (0.000 sec)

Query OK, 1 row affected (0.000 sec)

Database changed
Query OK, 0 rows affected (0.076 sec)

Query OK, 1 row affected (0.006 sec)

+-----------+--------+---------------------+-----------------------+
| idPayment | amount | paymentDate         | accountNumber         |
+-----------+--------+---------------------+-----------------------+
|         1 |   1295 | 2020-01-25 16:31:19 | A fake paypal account |
+-----------+--------+---------------------+-----------------------+
1 row in set (0.000 sec)

Query OK, 0 rows affected (0.032 sec)

Query OK, 1 row affected (0.006 sec)

+-----------+--------+---------------------+---------------------+----------------+
| idPayment | amount | paymentDate         | cardNumber          | expirationDate |
+-----------+--------+---------------------+---------------------+----------------+
|         2 |    245 | 2020-01-25 16:31:19 | 1234 5678 9012 3456 | 06/19          |
+-----------+--------+---------------------+---------------------+----------------+
1 row in set (0.000 sec)

MariaDB [Inheritance]>

Le mapping associé à cette stratégie

Nous allons continuer à utiliser l'annotation @Inheritance, mais cette fois-ci la stratégie sera @Inheritance( strategy = InheritanceType.TABLE_PER_CLASS ). Comme nous n'avons plus de jointures, il n'est plus utile d'utiliser l'annotation @PrimaryKeyJoinColumn et comme la classe Payment est abstraite, elle ne sera pas associée directement à une table.

Voici, de manière synthétique, l'ensemble des annotations JPA à porter sur nos différentes classes pour exploiter cette nouvelle stratégie.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
@Entity
@Inheritance( strategy = InheritanceType.TABLE_PER_CLASS )
public abstract class Payment {

    @Id @GeneratedValue(strategy=GenerationType.SEQUENCE)
    private int idPayment = 0;
    
    // Suite de la classe ...

}

@Entity 
@Table( name = "T_PaypalPayments" )
public class PaypalPayment extends Payment {
    
    // Suite de la classe ...

}

@Entity 
@Table( name = "T_CreditCardPayments" )
public class CreditCardPayment extends Payment {
    
    // Suite de la classe ...

}
Extraits de code représentant les différentes annotations JPA à utiliser
avec cette stratégie, il n'est plus possible de laisser la base de données produire les valeurs de clés primaires. Comme les entités sont réparties sur plusieurs tables, on pourrait avoir deux paiements de types différents avec des clés primaires identiques. Du coup, c'est l'ORM qui prend la responsabilité de générer une séquence de clé primaire unique. C'est pour cette raison que l'attribut idPayment de la classe Payment a été annoté différemment.

Et voici maintenant le code complet (avec les imports requis) de la classe Payment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
package fr.koor.webstore.business;

import java.util.Date;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;

@Entity
@Inheritance( strategy = InheritanceType.TABLE_PER_CLASS )
public abstract class Payment {

    @Id @GeneratedValue(strategy=GenerationType.SEQUENCE)
    private int idPayment = 0;
    private double amount = 0;
    private Date paymentDate = new Date();

    public Payment() {
    }

    public int getIdPayment() {
        return idPayment;
    }

    public void setIdPayment(int idPayment) {
        this.idPayment = idPayment;
    }

    public double getAmount() {
        return amount;
    }

    public void setAmount(double amount) {
        this.amount = amount;
    }

    public Date getPaymentDate() {
        return paymentDate;
    }

    public void setPaymentDate(Date paymentDate) {
        this.paymentDate = paymentDate;
    }

    @Override
    public String toString() {
        return "Payment: idPayment=" + idPayment + ", amount=" + 
               amount + ", paymentDate=" + paymentDate;
    }
    
}
La classe Payment et son mapping JPA

Voici à présent le code complet de la classe PaypalPayment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
package fr.koor.webstore.business;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity 
@Table( name = "T_PaypalPayments" )
public class PaypalPayment extends Payment {

    private String accountNumber = "unknown";
    
    public PaypalPayment() {
        super();
    }
    
    public String getAccountNumber() {
        return accountNumber;
    }
    
    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }
    
    @Override
    public String toString() {
        return super.toString() + ", accountNumber=" + accountNumber;
    }
}
La classe PaypalPayment et son mapping JPA

Et voici le code complet de la classe CreditCardPayment.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
package fr.koor.webstore.business;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity 
@Table( name = "T_CreditCardPayments" )
public class CreditCardPayment extends Payment {

    private String cardNumber = "unknown";
    private String expirationDate = "mm/yy";
    
    public CreditCardPayment() {
        super();
    }
    
    public String getCardNumber() {
        return cardNumber;
    }
    
    public void setCardNumber(String cardNumber) {
        this.cardNumber = cardNumber;
    }
    
    public String getExpirationDate() {
        return expirationDate;
    }
    
    public void setExpirationDate(String expirationDate) {
        this.expirationDate = expirationDate;
    }
    
    @Override
    public String toString() {
        return super.toString() + ", cardNumber=" + cardNumber + ", expirationDate=" + expirationDate;
    }
}
La classe CreditCardPayment et son mapping JPA

Test de notre mapping

Pour tester votre mapping, vous pouvez reprendre le main utilisé avec la stratégie précédente : normalement, les mêmes résultats devraient être produits.

[main] WARN  | org.hibernate.orm.connections.pooling                        | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70)
fr.koor.webstore.business.PaypalPayment
    Payment: idPayment=1, amount=1295.0, paymentDate=2020-01-25 10:43:24.0, accountNumber=A fake paypal account
fr.koor.webstore.business.CreditCardPayment
    Payment: idPayment=2, amount=245.0, paymentDate=2020-01-25 10:43:24.0, cardNumber=1234 5678 9012 3456, expirationDate=06/19


Mapping des collections Exemple d'un mapping complet