Spring Data JPA
Dans une application backend, on a (presque) toujours besoin de persister des données dans un système de gestion de base de donnée. On pourrait écrire des requêtes JDBC pour tout mais c'est du code super long et répétitif. Pour pallier ce problème, il existe JPA (Java Persistance API), une spécification d'ORM pour Java. Un ORM (Object Relationnal Mapper) est un composant logiciel qui va traduire automatiquement des instances d'objets du modèle orienté objet, en ligne d'enregistrement dans le modèle relationnel. JPA étant une spécification, nous avons besoin d'une implémentation, nous allons utiliser la plus connue qui s'appelle Hibernate.
Installer les dépendances
Pour installer JPA et Hibernate, rien de plus simple, il suffit d'ajouter la dépendance suivante à notre fichier pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
Il nous faut aussi un Driver JDBC, car ce dernier sera utilisé par Hibernate sous le capot.
Selon votre SGBD :
MySQL
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
Postgres
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
Oracle
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>21.1.0.0</version>
</dependency>
Configurer la connexion
Afin de configurer la connexion à la base de donnée, ouvrez le fichier application.properties
situé dans src/main/resources
. Et ajoutez les entrées suivantes :
spring.datasource.url=ici l'url de ma base
spring.datasource.username=ici le login de ma base
spring.datasource.password=ici le mot de passe de ma base
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
Les Entités
Les entités sont les objets qui sont persistés par l'ORM dans la base de donnée.
Pour définir nos classes d'objets qui seront enregistrées dans la base de donnée, il faut les définir en tant qu'entité. Cela se fait à l'aide de l'annotation @Entity
sur la classe. Une entité doit avoir un champs qui correspondra à se clé primaire dans la base de donnée, il est désigné avec l'annotation @Id
. l'ORM peut le générer automatiquement, et même aléatoirement dans le cas d'une UUID (voir exemple).
Les autres champs de la classe entités sont persistés automatiquement. Pour qu'un champs ne soit pas persisté, il faut l'annoter avec @Transient
.
Exemple d'entité pour notre Todo :
@Entity
public class Todo {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
private String title;
private String description;
... getters & setters ...
}
Les annotations @GeneratedValue
et GenericGenerator
permet de générer automatiquement un UUID aléatoire pour l'entité.
On peut aussi définir des contraintes sur les colonnes en rajoutant l'annotation @Column
sur le champs et en lui passant des paramètres, par exemple :
@Column(unique = true, length = 32)
Cela permet de mettre la contrainte "unique" sur le champs et d'imposer une longueur maximum de 32 caractère
Les Repository
Pour interagir avec les entités, il faut créer des Repository. Ce sont des interface que nous allons définir, mais qui seront implémentées non pas nous, mais pas l'ORM Hibernate. Ils peuvent ensuite être injectés dans les services par le conteneur d'injection de dépendances. Une interface Repository gère les interactions pour une entité et doit étendre l'interface JpaRepository
en fournissant en paramètre de type, le type de l'entité, ainsi que le type de son Id.
Exemple :
public interface TodoRepository extends JpaRepository<Todo, String> {
}
Méthodes de base
Voilà, juste en étendant cette interface, on peut accèder à tout un tas de méthodes intéressantes. Les principales sont :
-
save
: Sauvegarder (créer ou mettre à jours) une instance d'une entités -
findById
: récupérer une entité à partir de son Id -
findAll
: récupérer toutes les entités contenues dans la base (⚠ attention ça peut faire beaucoup de tout charger dans la mémoire) -
deletebyId
: supprimer une entité à partir de son Id
Et bien d'autre qui peuvent être utile, à retrouver dans la javadoc de l'interface
Méthodes Custom
On peut également définir de nouvelles méthodes dans l'interface pour filtrer sur d'autre champs, en nommant ces méthode de façon particulière :
findByTitle(String title);
findBy
veut dire qu'on filtre sur un champs et Title
est le nom d'un champs de notre entité, cette requête va donc récupérer les Todos qui ont le titre passé en paramètre.
Voici une références des mots utilisables dans les noms de méthode
Mot | Exemple | Logique SQL correspondante |
---|---|---|
And | findByLastnameAndFirstname | … where x.lastname = ?1 and x.firstname = ?2 |
Or | findByLastnameOrFirstname | … where x.lastname = ?1 or x.firstname = ?2 |
Is, Equals | findByFirstname,findByFirstnameIs,findByFirstnameEquals | … where x.firstname = ?1 |
Between | findByStartDateBetween | … where x.startDate between ?1 and ?2 |
LessThan | findByAgeLessThan | … where x.age < ?1 |
LessThanEqual | findByAgeLessThanEqual | … where x.age <= ?1 |
GreaterThan | findByAgeGreaterThan | … where x.age > ?1 |
GreaterThanEqual | findByAgeGreaterThanEqual | … where x.age >= ?1 |
After | findByStartDateAfter | … where x.startDate > ?1 |
Before | findByStartDateBefore | … where x.startDate < ?1 |
IsNull, Null | findByAge(Is)Null | … where x.age is null |
IsNotNull, NotNull | findByAge(Is)NotNull | … where x.age not null |
Like | findByFirstnameLike | … where x.firstname like ?1 |
NotLike | findByFirstnameNotLike | … where x.firstname not like ?1 |
StartingWith | findByFirstnameStartingWith | … where x.firstname like ?1 (parameter bound with appended %) |
EndingWith | findByFirstnameEndingWith | … where x.firstname like ?1 (parameter bound with prepended %) |
Containing | findByFirstnameContaining | … where x.firstname like ?1 (parameter bound wrapped in %) |
OrderBy | findByAgeOrderByLastnameDesc | … where x.age = ?1 order by x.lastname desc |
Not | findByLastnameNot | … where x.lastname <> ?1 |
In | findByAgeIn(Collection |
… where x.age in ?1 |
NotIn | findByAgeNotIn(Collection |
… where x.age not in ?1 |
True | findByActiveTrue() | … where x.active = true |
False | findByActiveFalse() | … where x.active = false |
IgnoreCase | findByFirstnameIgnoreCase | … where UPPER(x.firstname) = UPPER(?1) |
Relations
Les relations permettent de faire des liens entre les entités. Il existe quatres types de relations, définies par des annotations :
-
@ManyToOne
: plusieurs instances de cette classe entité sont en relation avec une unique instance d'une autre (ex: Pages d'un livre - plusieurs pages sont reliées à un unique livre) -
OneToMany
: une unique instance de cette classe entité est en relations avec plusieurs instances d'une autre (ex: Livre qui contient des pages - un unique livre est relié à plusieurs pages) -
@ManyToMany
: plusieurs instance de cette classe entité sont en relations avec plusieurs isntance d'une autre (ex: Classes et Professeurs - Les professeurs ont plusieurs classes et les classes ont plusieurs professeurs. -
@OneToOne
: une unique instance de cette classe entités est reliée à une unique instance d'une autre (ex: Dircteur et Ecole - une directeur dirige une seule école et une école est dirrigée par un seul directeur).
Exemple :
@Entity
public class Todo {
... autres propriétés ...
@ManyToOne
private ApplicationUser owner;
... getters & setters ....
}
@Entity
public class ApplicationUser {
@Id
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "uuid2")
private String id;
private String name;
@OneToMany
private Set<Todo> todos;
... getters & setters ...
}
Les annotations de relation possèdes plusieurs paramètres utiles à connaitres :
-
fetch
:-
FetchType.EAGER
: charge les données de la relation directement (défaut pour@ManyToOne
et@OneToOne
) -
FtechType.LAZY
: charge les données de la relation que quand le getter est appelé (défaut pour@OneToMany
et@ManyToMany
-
-
orphanRemoval
:true
oufalse
, détermine dans un@OneToMany
si les enfant doivent être supprimés quand le parent est supprimé → un enfant ne peut exister sans parent.
No Comments