Dernière modification : 07/02/2021

Spring JDBC (Java Database Connectivity)

Dans cet article nous allons aborder les différents mécanismes d'utilisation de JDBC (Java Database Connectivity) avec Spring.

1. Configuration

Commençons par la configuration de la source des données (DataSource). Nous utiliserons dans cet article une base de données PostgreSQL :

@Configuration
public class JpaConfiguration {

    Properties additionalProperties() {
        final Properties properties = new Properties();
        properties.setProperty("hibernate.hbm2ddl.auto", "update");

        return properties;
    }

    @Bean
    public DataSource dataSource() {
        final DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl("jdbc:postgresql://127.0.0.1:5432/laulemcms");
        dataSource.setUsername("laulemcms");
        dataSource.setPassword("laulemcms");

        return dataSource;
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        final LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
        em.setDataSource(dataSource);
        em.setPackagesToScan("com.laulem.springboot.model");

        final JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        em.setJpaVendorAdapter(vendorAdapter);
        em.setJpaProperties(this.additionalProperties());

        return em;
    }
}

Nous avons également défini ci-dessus le Bean LocalContainerEntityManagerFactoryBean. Dans celui-ci nous ajoutons à la configuration la demande de création ou la mise à jour des tables (hibernate.hbm2ddl.auto).

Une base de données H2 peut également être utilisée :

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:jdbc-script/rows.sql").build();
}

2. JdbcTemplate

2.1 Requête de base

Voyons dans un premier temps un exemple basique de JdbcTemplate.

Instanciation du JdbcTemplate :

private JdbcTemplate jdbcTemplate;

@Autowired
public void setDataSource(final DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);
}

Ci-dessous une requête de type SELECT :

public int getCountOfBooks() {
    return this.jdbcTemplate.queryForObject("SELECT COUNT(*) FROM BOOK", Integer.class);
}

Le premier paramètre est la requête à exécuter et le second est le type de données retourné par celle-ci.

Ci-dessous une requête de type INSERT :

public int addBook(String title) {
    return this.jdbcTemplate.update("INSERT INTO BOOK VALUES (?, ?)", this.getCountOfBooks() + 1, title);
}

Le premier paramètre est la requête à exécuter, les suivants sont les données à utiliser dans celle-ci. En effet, les "?" seront remplacés par ces données dans l'ordre d'apparition.

Remarque : Cette syntaxe permet d'éviter les injections SQL.

2.2 Requêtes nommées

En vue d'obtenir la prise en charge des paramètres nommés, nous utiliserons un autre modèle JDBC fourni par le framework : NamedParameterJdbcTemplate. Il permet de remplacer le traditionnel paramètre "?" par un paramètre ayant un nom. Par conséquent, le paramètre peut être utilisé plusieurs fois dans la requête SQL alors qu'il n'est renseigné qu'une seule fois.

Instanciation du NamedParameterJdbcTemplate :

private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

@Autowired
public void setDataSource(final DataSource dataSource) {
    this.jdbcTemplate = new JdbcTemplate(dataSource);

    this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}

Ci-dessous une requête de type SELECT avec un paramètre nommé :

public Long getIDByTitle(String title) {
    final SqlParameterSource namedParameters = new MapSqlParameterSource().addValue("title", title);
    return this.namedParameterJdbcTemplate.queryForObject("SELECT ID FROM BOOK WHERE TITLE = :title", namedParameters, Long.class);
}

Nous pouvons également utiliser une instance de la classe Book afin de récupérer des données au lieu de définir chacun des paramètres indépendamment comme ci-dessus. Exemple :

public Long getIDByTitle(String title) {
    final Book book = new Book();
    book.setTitle(title);

    final SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(book);
    return this.namedParameterJdbcTemplate.queryForObject("SELECT ID FROM BOOK WHERE TITLE = :title", namedParameters, Long.class);
}

2.3 Mapping des données en sortie

Nous avons vu plus haut des résultats simples des requêtes (Long). Désormais nous allons voir comment complexifier ces résultats. En effet, nous allons observer comment avoir un retour dans une instance de la classe Book.

Pour ce faire, créer un mapping en utilisant l'interface RowMapper :

public class BookRowMapper implements RowMapper {
    @Override
    public Book mapRow(ResultSet rs, int rowNum) throws SQLException {
        final Book book = new Book();

        book.setId(rs.getLong("ID"));
        book.setTitle(rs.getString("TITLE"));

        return book;
    }
}

Puis, utiliser ce mapping en sortie de notre appel à la base de données :

public Book getBookByTitle(String title) {
    final Book book = new Book();
    book.setTitle(title);

    final SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(book);
    return this.namedParameterJdbcTemplate.queryForObject("SELECT * FROM BOOK WHERE TITLE = :title", namedParameters, new BookRowMapper());
}

L'insertion est effectuée correctement dans la base de données.

3. Insertion par lots

Si l'on souhaite insérer un nombre conséquent de livres dans la base de données, il peut être intéressant de procéder avec BatchPreparedStatementSetter :

public int[] addBooks(List books) {
    return this.jdbcTemplate.batchUpdate("INSERT INTO BOOK VALUES (?, ?)",
            new BatchPreparedStatementSetter() {
                @Override
                public int getBatchSize() {
                    return books.size();
                }

                @Override
                public void setValues(PreparedStatement ps, int i) throws SQLException {
                    ps.setLong(1, books.get(i).getId());
                    ps.setString(2, books.get(i).getTitle());
                }
            });
}

Il est également possible d'insérer les données en utilisant les paramètres nommés :

public int[] addBooks(List books) {
    final SqlParameterSource[] batch = SqlParameterSourceUtils.createBatch(books);
    return this.namedParameterJdbcTemplate.batchUpdate("INSERT INTO BOOK VALUES (:id, :title)", batch);
}

4. Simplification des insertions

La classe SimpleJdbcInsert permet de simplifier les insertions avec une configuration minimale. En effet, il n'y a pas besoin de créer la requête SQL, il est simplement nécessaire de fournir la table ainsi que les valeurs à intégrer :

Instanciation du SimpleJdbcInsert :

private SimpleJdbcInsert simpleJdbcInsert;

@Autowired
public void setDataSource(final DataSource dataSource) {
    this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource).withTableName("BOOK");
}

Insertion d'un livre :

public int addBook(String title) {
    final Map parameters = new HashMap<>();
    parameters.put("ID", this.getCountOfBooks() + 1);
    parameters.put("TITLE", title);

    return this.simpleJdbcInsert.execute(parameters);
}