Persistance Java avec JPA et Hibernate, Partie 1: Entités et relations

L'API Java Persistence (JPA) est une spécification Java qui comble le fossé entre les bases de données relationnelles et la programmation orientée objet. Ce didacticiel en deux parties présente JPA et explique comment les objets Java sont modélisés en tant qu'entités JPA, comment les relations d'entité sont définies et comment utiliser les JPA EntityManageravec le modèle Repository dans vos applications Java.

Notez que ce tutoriel utilise Hibernate comme fournisseur JPA. La plupart des concepts peuvent être étendus à d'autres frameworks de persistance Java.

Qu'est-ce que JPA?

Voir «Qu'est-ce que JPA? Introduction à l'API Java Persistence» pour en savoir plus sur l'évolution de JPA et des frameworks associés, y compris EJB 3.0. et JDBC.

Relations d'objet dans JPA

Les bases de données relationnelles existent comme moyen de stockage des données de programme depuis les années 1970. Alors que les développeurs ont aujourd'hui de nombreuses alternatives à la base de données relationnelle, ce type de base de données est évolutif et bien compris, et est encore largement utilisé dans le développement de logiciels à petite et grande échelle.

Les objets Java dans un contexte de base de données relationnelle sont définis comme des entités . Les entités sont placées dans des tables où elles occupent des colonnes et des lignes. Les programmeurs utilisent des clés étrangères et des tables de jointure pour définir les relations entre les entités - à savoir les relations un-à-un, un-à-plusieurs et plusieurs-à-plusieurs. Nous pouvons également utiliser SQL (Structured Query Language) pour récupérer et interagir avec des données dans des tables individuelles et sur plusieurs tables, en utilisant des contraintes de clé étrangère. Le modèle relationnel est plat, mais les développeurs peuvent écrire des requêtes pour récupérer des données et construire des objets à partir de ces données.

Inadéquation d'impédance des relations objet

Vous connaissez peut-être le terme non -concordance d'impédance des relations d'objet , qui fait référence au défi de mapper des objets de données à une base de données relationnelle. Cette discordance se produit car la conception orientée objet n'est pas limitée aux relations un-à-un, un-à-plusieurs et plusieurs-à-plusieurs. Au lieu de cela, dans la conception orientée objet, nous pensons aux objets, à leurs attributs et à leur comportement, et à la relation entre les objets. Deux exemples sont l'encapsulation et l'héritage:

  • Si un objet contient un autre objet, nous définissons cela par encapsulation --Un a-une relation.
  • Si un objet est une spécialisation d'un autre objet, nous définissons cela par héritage --un est-une relation.

L'association, l'agrégation, la composition, l'abstraction, la généralisation, la réalisation et les dépendances sont tous des concepts de programmation orientés objet qui peuvent être difficiles à mapper à un modèle relationnel.

ORM: mapping objet-relationnel

Le décalage entre la conception orientée objet et la modélisation de base de données relationnelle a conduit à une classe d'outils développés spécifiquement pour le mappage objet-relationnel (ORM). Les outils ORM comme Hibernate, EclipseLink et iBatis traduisent les modèles de base de données relationnelle, y compris les entités et leurs relations, en modèles orientés objet. Beaucoup de ces outils existaient avant la spécification JPA, mais sans standard, leurs fonctionnalités dépendaient du fournisseur.

Lancée pour la première fois dans le cadre d'EJB 3.0 en 2006, l'API Java Persistence (JPA) offre un moyen standard d'annoter des objets afin qu'ils puissent être mappés et stockés dans une base de données relationnelle. La spécification définit également une construction commune pour interagir avec les bases de données. Avoir une norme ORM pour Java apporte de la cohérence aux implémentations des fournisseurs, tout en permettant également de la flexibilité et des modules complémentaires. Par exemple, alors que la spécification JPA originale est applicable aux bases de données relationnelles, certaines implémentations de fournisseurs ont étendu JPA pour une utilisation avec les bases de données NoSQL.

Évolution de JPA

La première version de JPA, version 1.0, a été publiée en 2006 via le Java Community Process (JCP) en tant que Java Specification Request (JSR) 220. La version 2.0 (JSR 317) a été publiée en 2009, la version 2.1 (JSR 338) en 2013, et la version 2.2 (une version de maintenance de JSR 338) a été publiée en 2017. JPA 2.2 a été sélectionné pour inclusion et développement continu à Jakarta EE.

Premiers pas avec JPA

L'API Java Persistence est une spécification, pas une implémentation: elle définit une abstraction commune que vous pouvez utiliser dans votre code pour interagir avec les produits ORM. Cette section passe en revue certaines des parties importantes de la spécification JPA.

Vous apprendrez à:

  • Définissez les entités, les champs et les clés primaires dans la base de données.
  • Créez des relations entre les entités de la base de données.
  • Travaillez avec le EntityManageret ses méthodes.

Définition des entités

Afin de définir une entité, vous devez créer une classe annotée avec l' @Entityannotation. L' @Entityannotation est une annotation de marqueur , utilisée pour découvrir les entités persistantes. Par exemple, si vous vouliez créer une entité de livre, vous l'annoteriez comme suit:

 @Entity public class Book { ... } 

Par défaut, cette entité sera mappée à la Booktable, comme déterminé par le nom de classe donné. Si vous souhaitez mapper cette entité à une autre table (et, éventuellement, à un schéma spécifique), vous pouvez utiliser l' @Tableannotation pour le faire. Voici comment mapper la Bookclasse à une table BOOKS:

 @Entity @Table(name="BOOKS") public class Book { ... } 

Si la table BOOKS était dans le schéma PUBLISHING, vous pouvez ajouter le schéma à l' @Tableannotation:

 @Table(name="BOOKS", schema="PUBLISHING") 

Mappage des champs aux colonnes

Avec l'entité mappée à une table, votre prochaine tâche consiste à définir ses champs. Les champs sont définis comme des variables membres dans la classe, le nom de chaque champ étant mappé à un nom de colonne dans la table. Vous pouvez remplacer ce mappage par défaut en utilisant l' @Columnannotation, comme illustré ici:

 @Entity @Table(name="BOOKS") public class Book { private String name; @Column(name="ISBN_NUMBER") private String isbn; ... } 

Dans cet exemple, nous avons accepté le mappage par défaut pour l' nameattribut, mais spécifié un mappage personnalisé pour l' isbnattribut. L' nameattribut sera mappé à la colonne de nom , mais l' isbnattribut sera mappé à la colonne ISBN_NUMBER.

L' @Columnannotation nous permet de définir des propriétés supplémentaires du champ / colonne, y compris la longueur, s'il est nullable, s'il doit être unique, sa précision et son échelle (s'il s'agit d'une valeur décimale), s'il est insérable et actualisable, et ainsi de suite .

Spécifier la clé primaire

L'une des conditions requises pour une table de base de données relationnelle est qu'elle doit contenir une clé primaire ou une clé qui identifie de manière unique une ligne spécifique dans la base de données. Dans JPA, nous utilisons l' @Idannotation pour désigner un champ comme clé primaire de la table. La clé primaire doit être un type primitif Java, un wrapper primitif, tel que Integerou Long, a String, a Date, a BigIntegerou a BigDecimal.

Dans cet exemple, nous mappons l' idattribut, qui est un Integer, à la colonne ID dans la table BOOKS:

 @Entity @Table(name="BOOKS") public class Book { @Id private Integer id; private String name; @Column(name="ISBN_NUMBER") private String isbn; ... } 

Il est également possible de combiner l' @Idannotation avec l' @Columnannotation pour écraser le mappage de nom de colonne de la clé primaire.

Relations entre entités

Maintenant que vous savez comment définir une entité, voyons comment créer des relations entre entités. JPA définit quatre annotations pour définir les entités:

  • @OneToOne
  • @OneToMany
  • @ManyToOne
  • @ManyToMany

Relations individuelles

The @OneToOne annotation is used to define a one-to-one relationship between two entities. For example, you may have a User entity that contains a user's name, email, and password, but you may want to maintain additional information about a user (such as age, gender, and favorite color) in a separate UserProfile entity. The @OneToOne annotation facilitates breaking down your data and entities this way.

The User class below has a single UserProfile instance. The UserProfile maps to a single User instance.

 @Entity public class User { @Id private Integer id; private String email; private String name; private String password; @OneToOne(mappedBy="user") private UserProfile profile; ... } 
 @Entity public class UserProfile { @Id private Integer id; private int age; private String gender; private String favoriteColor; @OneToOne private User user; ... } 

The JPA provider uses UserProfile's user field to map UserProfile to User. The mapping is specified in the mappedBy attribute in the @OneToOne annotation.

One-to-many and many-to-one relationships

The @OneToMany and @ManyToOne annotations facilitate both sides of the same relationship. Consider an example where a Book can have only one Author, but an Author may have many books. The Book entity would define a @ManyToOne relationship with Author and the Author entity would define a @OneToMany relationship with Book.

 @Entity public class Book { @Id private Integer id; private String name; @ManyToOne @JoinColumn(name="AUTHOR_ID") private Author author; ... } 
 @Entity public class Author { @Id @GeneratedValue private Integer id; private String name; @OneToMany(mappedBy = "author") private List books = new ArrayList(); ... } 

In this case, the Author class maintains a list of all of the books written by that author and the Book class maintains a reference to its single author. Additionally, the @JoinColumn specifies the name of the column in the Book table to store the ID of the Author.

Many-to-many relationships

Finally, the @ManyToMany annotation facilitates a many-to-many relationship between entities. Here's a case where a Book entity has multiple Authors:

 @Entity public class Book { @Id private Integer id; private String name; @ManyToMany @JoinTable(name="BOOK_AUTHORS", [email protected](name="BOOK_ID"), [email protected](name="AUTHOR_ID")) private Set authors = new HashSet(); ... } 
 @Entity public class Author { @Id @GeneratedValue private Integer id; private String name; @ManyToMany(mappedBy = "author") private Set books = new HashSet(); ... } 

In this example, we create a new table, BOOK_AUTHORS, with two columns: BOOK_ID and AUTHOR_ID. Using the joinColumns and inverseJoinColumns attributes tells your JPA framework how to map these classes in a many-to-many relationship. The @ManyToMany annotation in the Author class references the field in the Book class that manages the relationship; namely the authors property.

That's a quick demo for a fairly complex topic. We'll dive further into the @JoinTable and @JoinColumn annotations in the next article.

Working with the EntityManager

EntityManager is the class that performs database interactions in JPA. It is initialized through a configuration file named persistence.xml. This file is found in the META-INF folder in your CLASSPATH, which is typically packaged in your JAR or WAR file. The persistence.xml file contains:

  • The named "persistence unit," which specifies the persistence framework you're using, such as Hibernate or EclipseLink.
  • A collection of properties specifying how to connect to your database, as well as any customizations in the persistence framework.
  • A list of entity classes in your project.

Let's look at an example.

Configuring the EntityManager

First, we create an EntityManager using the EntityManagerFactory retrieved from the Persistence class:

 EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("Books"); EntityManager entityManager = entityManagerFactory.createEntityManager(); 

In this case we've created an EntityManager that is connected to the "Books" persistence unit, which we've configured in the persistence.xml file.

The EntityManager class defines how our software will interact with the database through JPA entities. Here are some of the methods used by EntityManager:

  • find retrieves an entity by its primary key.
  • createQuery creates a Query instance that can be used to retrieve entities from the database.
  • createNamedQuery loads a Query that has been defined in a @NamedQuery annotation inside one of the persistence entities. Named queries provide a clean mechanism for centralizing JPA queries in the definition of the persistence class on which the query will execute.
  • getTransaction defines an EntityTransaction to use in your database interactions. Just like database transactions, you will typically begin the transaction, perform your operations, and then either commit or rollback your transaction. The getTransaction() method lets you access this behavior at the level of the EntityManager, rather than the database.
  • merge() adds an entity to the persistence context, so that when the transaction is committed, the entity will be persisted to the database. When using merge(), objects are not managed.
  • persist adds an entity to the persistence context, so that when the transaction is committed, the entity will be persisted to the database. When using persist(), objects are managed.
  • refresh refreshes the state of the current entity from the database.
  • flush synchronizes the state of the persistence context with the database.

Ne vous inquiétez pas de l'intégration de toutes ces méthodes à la fois. Vous apprendrez à les connaître en travaillant directement avec le EntityManager, ce que nous ferons plus dans la section suivante.