Tutoriel pour créer un CRUD avec Spring

Gokan EKINCI

Ce tutoriel s'adresse à tous ceux qui souhaitent créer un CRUD pour manipuler des données avec une base de données relationnelle. Ce tutoriel repose sur le framework Spring.

Pour réagir au contenu de ce tutoriel, un espace de dialogue vous est proposé sur le forum.

Commentez

- Titre : Tutoriel pour créer un CRUD avec Spring

- Auteur : Gokan EKINCI

- Parution : 11 mars 2019

- Mise à jour : 11 mars 2019

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2019 Gokan EKINCI. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Informations

- II. Présentation des API

- III. La classe entité et l'interface DAO

- IV. Pool de connexion avec la « DataSource »

- V. L'implémentation JDBC

- VI. L'implémentation JdbcTemplate

- VII. L'implémentation JPA

- VIII. L'implémentation Spring Data

- IX. Remerciements

I - Informations

Les sources de ce tutoriel sont présents dans le repository Git suivant :

https://github.com/eau-de-la-seine/tutorial-spring-database.git

Vous aurez besoin de Git, Java 8, Maven 3 si vous souhaitez récupérer et lancer l'application. Les requêtes SQL présentés sont suffisamment générique pour que vous puissiez utiliser le SGBDR de votre choix mais il faudra alors changer le driver dans les dépendances du pom.xml pour vous adapter, j'utiliserais PostgreSQL 11.

N'hésitez donc pas à cloner le repository tutorial-spring-database sur votre machine à partir de la commande Git ci-dessous :

git clone https://github.com/eau-de-la-seine/tutorial-spring-database.git

Et à utiliser la commande mvn clean install spring-boot:run pour démarrer le projet. N'hésitez pas à lire le tutoriel Maven 3 si vous souhaitez en apprendre plus sur ce dernier.

Enfin, le projet utilise la dernière version stable de Spring Boot à l'heure où j'écris ce tutoriel, soit la version 2.1.3.

II - Présentation des API

Un CRUD (« Create Read Update Delete » en anglais) permet de manipuler un objet afin de lire ou modifier une donnée en base. Nous verrons quatre implémentations différentes d'API pour mettre en place notre CRUD :

- JDBC (Java DataBase Connectivity) : L'API de base Java pour se connecter au bases de données relationnelle, permet de réaliser toutes les opérations SQL de votre base de données relationnelle.

- JdbcTemplate : Une classe Spring qui reprend JDBC et l'intègre au framework Spring.

- JPA (Java Peristence API) : L'API Java pour manipuler des objets Java avec une grande simplicité, cet API nous permet de s'abstraire des requêtes SQL pour réaliser des opérations de base. On peut utiliser des requêtes JPQL (Java Persistence Query Language) pour réaliser des requêtes SELECT plus avancées. Il est aussi possible des requêtes SQL natives en dernier recours mais JPA perd alors de son intérêt.

- Spring Data : Un concept Spring qui reprend JPA et l'intègre au framework Spring.

Dans une première étape nous créerons une classe entité ProductEntity à l'image de notre table dans la base de données, puis une interface IProductDAO pour manipuler nos objets de type ProductEntity. Nous créerons quatre implémentations pour IProductDAO : ProductDAOviaJDBC, ProductDAOviaJdbcTemplate, ProductDAOviaJPA, ProductDAOviaSpringData, que nous activerons à tour de rôle grâce aux profils Spring (observez la property « spring.profiles.active » dans le fichier application.properties). Nous verrons à travers ce tutoriel les avantages et les inconvénients de chacune de ces implémentations.

III - La classe entité et l'interface DAO

Pour notre classe entité « produit », créons une classe ProductEntity à l'image de notre table « products » dans la base :

public class ProductEntity {
private Long id;
private String name;
private String urlImage;
private Integer price;
private String description;
// Generate getters and setters
}

Cette classe « produit » contient un identifiant, un nom, une url pour donner accès à son image, un prix (en centimes) et une description. Voici la requête SQL que vous pouvez exécuter pour créer la table products dans votre base :

CREATE TABLE products (
id integer,
name varchar,
name varchar,
url_image varchar,
price integer,
description varchar,
CONSTRAINT pk_products PRIMARY KEY(id)
);

Reprenez l'interface ci-dessous, les implémentations de cette interface nous permettront de manipuler les données en base via des objets Java :

public interface IProductDAO {
ProductEntity insert(ProductEntity product) throws SQLException;
void update(ProductEntity product) throws SQLException;
void delete(ProductEntity product) throws SQLException;
ProductEntity select(ProductEntity product) throws SQLException;
List<ProductEntity> selectAll() throws SQLException;
}

Cette interface DAO (Data Access Object) contient les méthodes abstraites pour insérer, mettre à jour, supprimer, et récupérer un ou des éléments de la base sous forme d'objet Java.

IV - Pool de connexion avec la « DataSource »

La DataSource est un objet Java permettant d'optimiser les connexions effectuées avec la base de données, elle vous permet par exemple de gérer un certain nombre de connexion avec la base pour éviter de la saturer. Créer une classe de configuration DatabaseConfiguration pour créer cette DataSource :

@Configuration
@EnableTransactionManagement
public class DatabaseConfiguration {
@Bean
@ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}
}

Ici, l'annotation @ConfigurationProperties va récupérer toutes les properties commençant par « spring.datasource » dans le fichier application.properties de Spring :

dbms.port=5432
dbms.database.name=public
spring.datasource.url=jdbc:postgresql://localhost:${dbms.port}/${dbms.database.name}
spring.datasource.jdbc-url=${spring.datasource.url}
spring.datasource.username=postgres
spring.datasource.password=postgres
spring.datasource.driver-class-name=org.postgresql.Driver

Vous ferez attention à ne pas laisser des informations sensibles en dur dans le code comme je le fais dans ce tutoriel.

V - L'implémentation JDBC

Voici la première implémentation JDBC pour notre interface DAO (cliquez pour dérouler le code) :

@Repository
@Profile("jdbc")
public class ProductDAOviaJDBC implements IProductDAO {

private final String SQL_INSERT;
private final String SQL_UPDATE;
private final String SQL_DELETE;
private final String SQL_SELECT;
private final String SQL_SELECT_ALL;
private final DataSource dataSource;

public ProductDAOviaJDBC(
DataSource dataSource,
@Value("${tp.jdbc.SQL_INSERT}") String sqlInsert,
@Value("${tp.jdbc.SQL_UPDATE}") String sqlUpdate,
@Value("${tp.jdbc.SQL_DELETE}") String sqlDelete,
@Value("${tp.jdbc.SQL_SELECT}") String sqlSelect,
@Value("${tp.jdbc.SQL_SELECT_ALL}") String sqlSelectAll
) {
SQL_INSERT = sqlInsert;
SQL_UPDATE = sqlUpdate;
SQL_DELETE = sqlDelete;
SQL_SELECT = sqlSelect;
SQL_SELECT_ALL = sqlSelectAll;
this.dataSource = dataSource;
}

@Override
public ProductEntity insert(ProductEntity product) throws SQLException {
try (
Connection c = createConnection();
PreparedStatement p = c.prepareStatement(SQL_INSERT, Statement.RETURN_GENERATED_KEYS);
) {
p.setString(1, product.getName());
p.setString(2, product.getUrlImage());
p.setInt(3, product.getPrice());
p.setString(4, product.getDescription());

int nbInsert = p.executeUpdate();

if (nbInsert == 1) {
try (ResultSet generatedKeys = p.getGeneratedKeys()) {
if (generatedKeys.next()) {
product.setId(generatedKeys.getLong(1));
return product;
}
}
}
}

return null;
}

@Override
public void update(ProductEntity product) throws SQLException {
try (
Connection c = createConnection();
PreparedStatement p = c.prepareStatement(SQL_UPDATE);
) {
p.setString(1, product.getName());
p.setString(2, product.getUrlImage());
p.setInt(3, product.getPrice());
p.setString(4, product.getDescription());
p.setLong(5, product.getId());

p.executeUpdate();
}
}

@Override
public ProductEntity select(ProductEntity product) throws SQLException {
try (
Connection c = createConnection();
PreparedStatement p = c.prepareStatement(SQL_SELECT);
) {
p.setLong(1, product.getId());

try (
ResultSet r = p.executeQuery();
) {
if (r.next()) {
ProductEntity productFromDb = new ProductEntity();
productFromDb.setId(r.getLong("id"));
productFromDb.setName(r.getString("name"));
productFromDb.setUrlImage(r.getString("url_image"));
productFromDb.setPrice(r.getInt("price"));
productFromDb.setDescription(r.getString("description"));
return productFromDb;
}

return null;
}
}
}

@Override
public List<ProductEntity> selectAll() throws SQLException {
try (
Connection c = createConnection();
PreparedStatement p = c.prepareStatement(SQL_SELECT_ALL);
) {
try (
ResultSet r = p.executeQuery();
) {
List<ProductEntity> list = new ArrayList<>();
while (r.next()) {
ProductEntity productFromDb = new ProductEntity();
productFromDb.setId(r.getLong("id"));
productFromDb.setName(r.getString(2));
productFromDb.setUrlImage(r.getString(3));
productFromDb.setPrice(r.getInt(4));
productFromDb.setDescription(r.getString(5));
list.add(productFromDb);
}

return list;
}
}
}

/**
* Syntaxe Java 6
*
* @param product
* @throws SQLException
*/
@Override
public void delete(ProductEntity product) throws SQLException {
Connection c = createConnection();
PreparedStatement p = null;
try {
p = c.prepareStatement(SQL_DELETE);
p.setLong(1, product.getId());
p.executeUpdate();
} finally {
if (p != null) {
p.close();
}

if (c != null) {
c.close();
}
}
}

public Connection createConnection() throws SQLException {
return dataSource.getConnection();
}
}

Vous remarquerez que cette implémentation est longue et fastidieuse à écrire, nous devons gérer les requêtes SQL, l'initialisation de chacun des champs de notre objet, l'ouverture et la fermeture des connexions.

La seule dépendance dont nous avons eu besoin pour construire cette implémentation est la DataSource.

VI - L'implémentation JdbcTemplate

Voici la deuxième implémentation JdbcTemplate pour notre interface DAO (cliquez pour dérouler le code) :

package fr.ekinci.demojpa.service;

import fr.ekinci.demojpa.model.ProductEntity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
@Profile("jdbcTemplate")
public class ProductDAOviaJdbcTemplate implements IProductDAO {

private final JdbcTemplate jdbcTemplate;
private final String SQL_UPDATE;
private final String SQL_DELETE;
private final String SQL_SELECT;
private final String SQL_SELECT_ALL;

public ProductDAOviaJdbcTemplate(
JdbcTemplate jdbcTemplate,
@Value("${tp.jdbc.SQL_UPDATE}") String sqlUpdate,
@Value("${tp.jdbc.SQL_DELETE}") String sqlDelete,
@Value("${tp.jdbc.SQL_SELECT}") String sqlSelect,
@Value("${tp.jdbc.SQL_SELECT_ALL}") String sqlSelectAll
) {
this.jdbcTemplate = jdbcTemplate;
this.SQL_UPDATE = sqlUpdate;
this.SQL_DELETE = sqlDelete;
this.SQL_SELECT = sqlSelect;
this.SQL_SELECT_ALL = sqlSelectAll;
}

@Override
public ProductEntity insert(ProductEntity product) throws SQLException {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate)
.withTableName("product")
.usingGeneratedKeyColumns(
/* PRIMARY_KEYS */ "id");

Map<String, Object> parameters = new HashMap<>();
parameters.put("name", product.getName());
parameters.put("url_image", product.getUrlImage());
parameters.put("price", product.getPrice());
parameters.put("description", product.getDescription());

product.setId(
(Long) jdbcInsert.executeAndReturnKey(parameters)
);

return product;
}

@Override
public void update(ProductEntity product) throws SQLException {
jdbcTemplate.update(SQL_UPDATE,
product.getName(),
product.getUrlImage(),
product.getPrice(),
product.getDescription(),
product.getId());
}

@Override
public void delete(ProductEntity product) throws SQLException {
jdbcTemplate.update(
SQL_DELETE,
product.getId());
}

@Override
public ProductEntity select(ProductEntity product) throws SQLException {
return jdbcTemplate.queryForObject(
SQL_SELECT,
new ProductRowMapper(),
product.getId());
}

@Override
public List<ProductEntity> selectAll() throws SQLException {
return jdbcTemplate.query(
SQL_SELECT_ALL,
new ProductRowMapper());
}

public static class ProductRowMapper implements RowMapper<ProductEntity> {
@Override
public ProductEntity mapRow(ResultSet rs, int rowNum) throws SQLException {
ProductEntity product = new ProductEntity();
product.setId(rs.getLong("id"));
product.setName(rs.getString("name"));
product.setUrlImage(rs.getString("url_image"));
product.setPrice(rs.getInt("price"));
product.setDescription(rs.getString("description"));
return product;
}
}
}

Vous remarquerez que cette implémentation est presque tout aussi longue et fastidieuse à écrire que JDBC, par contre nous déléguons certaines requêtes à JdbcTemplate et ne gérons plus l'ouverture et la fermeture des connexions.

La seule dépendance dont nous avons eu besoin pour construire cette implémentation est un JdbcTemplate (initialisé avec une DataSource), voici le code de DatabaseConfiguration à jour :

@Configuration
@EnableTransactionManagement
public class DatabaseConfiguration {
@Bean
@ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}

@Bean("jdbcTemplate")
@Profile("jdbcTemplate")
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}

VII - L'implémentation JPA

Les choses intéressantes commencent avec cette troisième implémentation. En effet, pour effectuer les opérations de base d'un CRUD, nous n'avons plus besoin de requête SQL, mais nous devons tout de même ajouter des informations à notre classe ProductEntity grâce aux annotations JPA :

@Entity
@Table(schema = "public", name = "products")
public class ProductEntity {

@Id
private Long id;

private String name;

@Column(name="url_image")
private String urlImage;

private Integer price;

private String description;
// Generate getters and setters
}

Voici quelques informations concernant ces annotations JPA :

@Entity

Déclare un EJB Entity Bean, soit une classe entité. Cette annotation est obligatoire

@Table

Indique le nom du schéma et de la table de notre classe entité

@Id

Indique la clé primaire de notre classe entité. Cette annotation est obligatoire

@Column

Indique le nom de la colonne en base, peut être utile si vous utilisez des underscores pour les noms de vos colonnes par exemple

Voici la troisième implémentation JPA pour notre interface DAO :

@Repository
@Profile("jpa")
public class ProductDAOviaJPA implements IProductDAO {

@PersistenceContext(unitName = DatabaseConfiguration.JPA_PERSISTANCE_UNIT_NAME)
private EntityManager em;

@Override
public ProductEntity insert(ProductEntity product) {
product.setId(null);
return em.merge(product);
}

@Override
public void update(ProductEntity product) {
em.merge(product);
}

@Override
public void delete(ProductEntity product) throws SQLException {
em.remove(product);
}

@Override
public ProductEntity select(ProductEntity product) {
return em.find(ProductEntity.class, product.getId());
}

@Override
public List<ProductEntity> selectAll() {
return em.createQuery("SELECT p FROM Product p", ProductEntity.class).getResultList();
}
}

Vous remarquerez que cette implémentation est bien plus simple à mettre en place que JDBC ou JdbcTemplate, nous ne gérons ni les requêtes SQL, ni l'initialisation des champs, ni l'ouverture et la fermeture des connexions.

La seule dépendance dont nous avons eu besoin pour construire cette implémentation est un EntityManager, voici le code de à jour (sans JdbcTemplate) :

@Configuration
@EnableTransactionManagement
// Analyze packages containing the Entity classes and Reposity interfaces/classes
@EnableJpaRepositories(basePackageClasses = {
ProductEntity.class,
ProductRepository.class
})
public class DatabaseConfiguration {
private static final String JPA_PACKAGES_TO_SCAN = "fr.ekinci.demojpa.model";
public static final String JPA_PERSISTANCE_UNIT_NAME = "demo-jpa-unit";

@Bean
@ConfigurationProperties(prefix="spring.datasource")
public DataSource dataSource() {
return DataSourceBuilder.create().build();
}

@Bean
@Profile("jpa")
public LocalContainerEntityManagerFactoryBean localContainerEntityManagerFactoryBean(
DataSource dataSource
) {
LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
emfb.setDataSource(dataSource);
emfb.setPackagesToScan(JPA_PACKAGES_TO_SCAN);
emfb.setPersistenceUnitName(JPA_PERSISTANCE_UNIT_NAME);
emfb.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
return emfb;
}
}

VIII - L'implémentation Spring Data

L'implémentation Spring Data reprend le meilleur de JPA et l'intègre au framework Spring. Pour mettre en place ce dernier rien de plus simple, créez l'interface ci-dessous :

import org.springframework.data.repository.CrudRepository;

public interface ProductRepository extends CrudRepository<ProductEntity, Long> {
}

Cette interface « ProductRepository » hérite de l'interface CrudRepository de Spring :

- L'interface possède deux paramètres génériques, le premier représente le type de la classe entité et le second le type de la clé primaire dans cette classe entité

- L'interface hérite d'une autre interface Repository<T, ID>annotée avec @Indexed pour être détectée par Spring. Nous ne créerons pas de classe héritant de notre interface ProductRepository, c'est la « magie » de Spring qui se chargera de nous fournir une implémentation lors de l'injection de dépendance

Voici la quatrième implémentation Spring Data de notre interface DAO :

@Repository
@Profile("springData")
public class ProductDAOviaSpringData implements IProductDAO {

private final ProductRepository productRepository;

public ProductDAOviaSpringData(ProductRepository productRepository) {
this.productRepository = productRepository;
}

@Override
public ProductEntity insert(ProductEntity product){
return productRepository.save(product);
}

@Override
public void update(ProductEntity product){
productRepository.save(product);
}

@Override
public void delete(ProductEntity product){
productRepository.delete(product);
}

@Override
public ProductEntity select(ProductEntity product){
return productRepository.findById(product.getId()).orElse(null);
}

@Override
public List<ProductEntity> selectAll(){
return (List<ProductEntity>) productRepository.findAll();
}
}

Vous remarquerez que cette implémentation est aussi simple à mettre en place que JPA. Pensez à importer la dépendance « spring-boot-starter-data-jpa » dans votre pom.xml et à initialiser vos properties « spring.datasource » dans le fichier application.properties

IX - Remerciements

Je tiens à remercier XXX pour la relecture orthographique de ce tutoriel.