Skip to content

6. [Cours]: Introdução ao API JDBC

Palavras-chave: bancos de dados relacionais, API JDBC, SQLException.

6.1. Support

A pasta [support / chap-06] contém os projetos do Eclipse deste capítulo.

6.2. Architecture

A camada JDBC (Java DataBase Connectivity) é uma interface universal de acesso a bancos de dados. Ela sempre apresenta a mesma interface para a camada [DAO]. Se houver uma mudança no SGBD, basta alterar o driver JDBC. A camada [DAO] permanece inalterada.

6.3. As etapas de operação de um banco de dados

Na arquitetura acima, a operação de um banco de dados pelo programa de console envolve as seguintes etapas:

  1. carregamento do driver JDBC do banco de dados;
  2. abertura de uma conexão com o banco de dados;
  3. emissão de um comando SQL no banco de dados e processamento dos resultados do comando SQL;
  4. encerramento da conexão;

A etapa 1 é realizada apenas uma vez. As etapas 2 a 4 são repetidas. Observe que não se deixa uma conexão aberta. Ela é fechada assim que não for mais necessária.

6.3.1. etapa 1 — carregamento na memória do driver JDBC

O código


        // carregando o driver JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // tratar a exceção
}

A operação da linha 3 tem como objetivo carregar na memória o driver JDBC do banco de dados. Essa operação precisa ser realizada apenas uma vez. Repeti-la, no entanto, não causa erros. A classe do driver JDBC é procurada no Classpath do projeto. Portanto, no projeto do Eclipse, o [jar], que contém a classe do driver JDBC, deve ter sido incluído no Classpath do projeto.

6.3.2. Etapa 2 – abertura de uma conexão

Assim que o driver JDBC estiver instalado, ele é solicitado a abrir uma conexão com o BD:

O código


package spring.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class IntroJdbc01 {

...
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // abertura da conexão
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // tratando a exceção
            ...
        } finally {
         // fechando a conexão
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // tratar a exceção
                ...
            }
         }
}
  • linhas 3-7: as classes de implementação da interface JDBC estão todas no pacote [java.sql]. Além disso, em caso de erro, todas elas lançam uma exceção do tipo [SQLException] (linhas 19 e 27). Essa exceção deriva da classe [Exception] e é uma exceção chamada de controlada: é obrigatório usar um try/catch para tratá-la ou, alternativamente, não tratá-la e indicar que o método permite que a exceção seja lançada, completando a assinatura do método com [throws SQLException];
  • linha 17, [DriverManager.getConnection] é um método estático que espera três parâmetros:
    • [url]: o URL do banco de dados. Trata-se de uma sequência de caracteres que depende do BD utilizado. Para MySQL, ela tem o formato [jdbc:mysql://localhost:3306/nom_de_la_bd];
    • [user]: o proprietário da conexão;
    • [passwd]: sua senha;
  • linhas 24-30: a conexão deve ser encerrada na cláusula [finally] para que seja encerrada independentemente de haver ou não uma exceção.

6.3.3. Etapa 3 – emissão de comandos SQL e [SELECT]

Uma vez estabelecida a conexão, é possível emitir ordens SQL. A forma de gerenciar ordens de leitura [SELECT] difere daquela utilizada para as operações de atualização [UPDATE, INSERT, DELETE]. Começamos pelas ordens SQL e [SELECT]:

O código


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // início da conexão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo somente leitura
            connexion.setReadOnly(true);
            // lendo a tabela [PRODUITS]
            ps = connexion.prepareStatement("SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS");
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
             doCatchException(connexion,e1);
        } finally {
            // tratando o finally
            doFinally(rs, ps, connexion);
        }

    private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
....
}
  • linhas 8, 10: abertura de uma transação (linha 8) no modo somente leitura (linha 10). Uma transação é uma sequência de ordens SQL que ou são todas bem-sucedidas ou todas falham. Assim, em uma transação com N ordens SQL, se a ordem I+1 falhar, as I anteriores serão canceladas. Para uma operação de leitura, não é necessária uma transação. No entanto, criar uma transação somente leitura pode permitir que certas ordens SGBD realizem algumas otimizações;
  • linha 12: uso de um [PreparedStatement]. Um [PreparedStatement] normalmente possui parâmetros indicados pelo caractere ?. Aqui, ele não os possui. Um [PreparedStatement] é uma ordem preparada pelo SGBD. Essa preparação tem um custo e é feita apenas uma vez. Em seguida, essa ordem preparada é executada pelo SGBD com diferentes parâmetros efetivos que substituirão os parâmetros formais ?. Vale ressaltar que é preferível nomear as colunas desejadas em vez de usar a notação * para obter todas as colunas. Ao especificar o nome das colunas, é possível obter seus valores a partir de sua posição na consulta SELECT;
  • linha 13: execução do [PreparedStatement]. Recupera-se um objeto do tipo [ResultSet];

Um objeto do tipo [ResultSet] representa uma tabela, ou seja, um conjunto de linhas e colunas. Em um determinado momento, só se tem acesso a uma linha da tabela, chamada de linha atual. Durante a criação inicial do [ResultSet], não há linha atual. É necessário executar uma operação [ResultSet.next()] para obtê-la. A assinatura do método next é a seguinte:

    boolean next()

Esse método tenta avançar para a próxima linha do [ResultSet] e retorna true se for bem-sucedido; caso contrário, retorna false. Se for bem-sucedido, a próxima linha se torna a nova linha atual. A linha anterior é perdida e não será possível voltar atrás para recuperá-la.

A tabela [ResultSet] possui colunas denominadas labelCol1, labelCol2, ... especificadas na consulta [SELECT] executada. Com a consulta:

SELECT ID as myId, NOM as myNom, CATEGORIE as myCategorie, PRIX as myPrix, DESCRIPTION as myDescription FROM PRODUITS
  • a coluna [ID] será transferida para uma coluna do [ResultSet] chamada [myId];
  • a coluna [NOM] será transferida para uma coluna do [ResultSet] chamada [myNom];
  • ...

No exemplo acima, os identificadores [myCol] são chamados de rótulos de coluna. Na ausência desses rótulos, os nomes das colunas do [ResultSet] dependem do SGBD. Quando o [SELECT] opera em uma única tabela, os rótulos das colunas serão, por padrão, os nomes das colunas solicitadas pelo SELECT. O problema surge quando o [SELECT] opera em várias tabelas e nessas tabelas existem nomes de colunas idênticos, como no exemplo a seguir:

SELECT PRODUITS.NOM, CATEGORIES.NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

supondo que a tabela [PRODUITS] tenha uma chave estrangeira para a tabela [CATEGORIES], representada pela relação [Produits].CATEGORIE_ID --> [CATEGORIES].ID, e que as tabelas [PRODUITS] e [CATEGORIES] tenham ambas um campo [NOM]. Nesse caso, os nomes atribuídos na tabela [ResultSet] às colunas [PRODUITS.NOM] e [CATEGORIES.NOM] dependem da tabela SGBD. Para a portabilidade entre o SGBD, é necessário, portanto, utilizar rótulos de colunas aqui, e escreveremos:


SELECT PRODUITS.NOM as p_NOM, CATEGORIES.NOM as c_NOM FROM PRODUITS, CATEGORIES WHERE PRODUITS.CATEGORIE_ID=CATEGORIES.ID

Para explorar os diferentes campos da linha atual do [ResultSet], dispomos dos seguintes métodos:

Type getType("labelColi") 

para obter a coluna denominada “labelColi” da linha atual e, portanto, a coluna do [SELECT] que possui esse rótulo. Type designa o tipo do campo coli. É possível utilizar os seguintes métodos [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Em vez de usar o nome da coluna, pode-se usar sua posição na consulta [SELECT] executada:

Type getType(i) 

onde i é o índice da coluna desejada (i>=1).

  • linhas 15-17: recuperação dos valores lidos na consulta BD;
  • linha 19: a transação é validada (também chamada de “confirmada”). Isso a encerra e libera os recursos que a transação SGBD havia mobilizado para ela;
  • linha 25: os recursos são liberados no [finally]. Este chama o método [doFinally] a seguir:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // fechamento de ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // encerramento [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        if (connexion != null) {
            try {
                // fechar a conexão
                connexion.close();
            } catch (SQLException e3) {
                 // tratar a exceção
            }
        }
    }
  • linhas 3-9: encerramento do [ResultSet];
  • linhas 11-17: encerramento do [PreparedStatement];
  • linhas 18-27: encerramento da conexão;

Os encerramentos nas linhas 3-17 parecem redundantes, uma vez que a conexão é encerrada nas linhas 18-25. Na verdade, em alguns casos, eles não são redundantes, e é recomendável mantê-los ([http://stackoverflow.com/questions/4507440/must-jdbc-resultsets-and-statements-be-closed-separately-although-the-connection]).

  • linha 22: a exceção é tratada pelo método [doCatchException] a seguir:

    private static void doCatchException(Connection connexion, Throwable th) {
        // cancelar transação
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // tratar a exceção
        }
}
  • linhas 4-6: a transação é cancelada. Isso a encerra, e o SGBD poderá liberar os recursos mobilizados para ela;

6.3.4. Etapa 3 — emissão das ordens SQL e [INSERT, UPDATE, DELETE]

As ordens SQL e [INSERT, UPDATE, DELETE] são operações de atualização: elas modificam o banco de dados, mas não retornam nenhuma linha. A única informação fornecida é o número de linhas afetadas pela operação de atualização.

O código


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // abrir conexão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // no modo leitura/gravação
            connexion.setReadOnly(false);
            // atualização da tabela
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // categoria 1
            ps.setInt(1, 10);
            // execução
            int nbLignes=ps.executeUpdate();
            // confirmação da transação
            connexion.commit();
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException(connexion, e1);
        } finally {
            // processando o finally
            doFinally(null, ps, connexion);
        }
    }
  • linha 9: a conexão é utilizada para leitura e gravação;
  • linha 11: um [PreparedStatement] com 1 parâmetro (simbolizado por ?). É possível ter vários parâmetros. Eles são numerados a partir de 1;
  • linha 13: atribui-se seu valor ao único parâmetro. O primeiro parâmetro de [setType] é a posição do parâmetro no [PreparedStatement] (1, 2, ...) e o segundo é o valor que lhe é atribuído. É possível utilizar os métodos [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • linha 15: utiliza-se o método [executeUpdate] e não o [executeQuery], reservado para as ordens SELECT. O método retorna o número de linhas afetadas pela operação. Pode ser 0.
  • linha 17: a transação é validada;

6.3.5. etapa 4 — encerramento da conexão

Uma conexão deve ser fechada o mais rápido possível em um contexto multiusuário, pois um SGBD aceita um número limitado de conexões abertas. Nos exemplos anteriores, ela era fechada na cláusula [finally] das operações SQL, de modo que fosse fechada independentemente de ter ocorrido uma exceção ou não.

6.4. Um projeto de exemplo

6.4.1. Suporte

A pasta [support / chap5] contém os projetos Eclipse deste capítulo [1, 2]. A pasta [database] contém o script SQL que permite criar o banco de dados de exemplo deste capítulo, MySQL.

6.4.2. O banco de dados utilizado

Os exemplos a seguir utilizam o banco de dados MySQL a seguir:

 
  • [ID]: chave primária no modo AUTO_INCREMENT (se não for fornecida uma chave primária, o SGBD a gera);
  • [NOM]: nome de um produto — único;
  • [CATEGORIE]: número da categoria;
  • [PRIX]: preço do produto;
  • [DESCRIPTION]: uma descrição do produto;

Vamos criá-la com a ferramenta [WampServer] da seguinte maneira: [1-9]:

6.4.3. O projeto Eclipse

  

O projeto é um projeto Maven definido pelo seguinte arquivo [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
    </dependencies>
</project>
  • linhas 8-12: o driver JDBC do SGBD MySQL5;
  • linhas 13-17: uma biblioteca capaz de lidar com jSON (Javascript Object Notation) (ver parágrafo 22.6). Vamos usá-la para exibir, no formato jSON, os produtos do banco de dados;

6.4.4. A classe dos produtos

A classe [Produit] é a seguinte:


package istia.st.jdbc;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Produit {

    // campos
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;

    // construtores
    public Produit() {

    }

    public Produit(int id, String nom, int categorie, double prix, String description) {
        this.id = id;
        this.nom = nom;
        this.categorie = categorie;
        this.prix = prix;
        this.description = description;
    }

    // getters e setters
    ...

    // para String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • linha 34: utilizamos a biblioteca jSON para exibir a string jSON do produto. Isso resulta em uma exibição semelhante à seguinte:
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}

A vantagem do método [toString] acima é que, se forem adicionados ou removidos campos da classe, seu método [toString] continua válido. Além disso, se os campos forem, por sua vez, objetos (listas, matrizes, dicionários, objetos de usuário), as bibliotecas jSON sabem transformá-los, por sua vez, em cadeias jSON;

6.4.5. A classe [Static]

A classe [Static] agrupa em métodos o código utilizado com frequência na classe principal:


package istia.st.jdbc;

import java.util.ArrayList;
import java.util.List;

public class Static {

    public static List<String> getErreursFromThrowable(Throwable th) {
        // recuperando a lista de mensagens de erro da exceção
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // mensagem de erro do throwable
            erreurs.add(th.getMessage());
            // passamos para a causa do throwable
            th = th.getCause();
        }
        // resultado
        return erreurs;
    }
    
    public static void show(String title, List<String> messages){
        // título
        System.out.println(String.format("%s : ",title));
        // mensagens
        for(String message : messages){
            System.out.println(String.format("- %s",message));
        }
    }
}
  • linhas 8-19: permite obter a lista de erros encapsulados em um objeto do tipo [Throwable], que é a classe-pai da classe [Exception];
  • linhas 21-28: exibe na tela uma lista de mensagens;

Esse código poderia estar na classe principal, pois, neste caso, ela é a única a utilizá-lo. Estamos, porém, diante de um caso mais amplo, em que outras classes precisariam desse código.

6.4.6. A estrutura da classe principal


package istia.st.jdbc;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class IntroJdbc01 {

    // constantes
    final static String url = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String user = "root";
    final static String passwd = "";
    final static String insert = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String delete = "DELETE FROM PRODUITS";
    final static String select = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String update = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String insert2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";

    public static void main(String[] args) {
        // carregando o driver JDBC de MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // esvazia-se a tabela [PRODUITS]
        delete();
        // preenchendo-a
        insert();
        // a tabela é lida
        select();
        // atualização
        update();
        // exibição
        select();
        // inserção de dois elementos idênticos
        // a inserção deve falhar e nenhum dos dois elementos é inserido devido à transação
        insert2();
        // verifica-se
        select();
        // concluído
        System.out.println("Travail terminé");
    }

    // lista de produtos
    private static void select() {
...
    }

    // exclusão de produtos
    public static void delete() {
...
    }

    // adição de produtos
    public static void insert() {
...
    }

    // adição de 2 produtos
    public static void insert2() {
...
    }

    // atualização de alguns produtos
    public static void update() {
..
    }

    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // encerramento ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {

            }
        }
        // encerramento [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {

            }
        }
        // encerrar a conexão
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // exibe as mensagens de erro
                Static.show("Les erreurs suivantes se sont produites lors de la fermeture de la connexion",
                        Static.getErreursFromThrowable(e3));
            }
        }
    }

    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // exibe as mensagens de erro
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancelamento da transação
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // exibe as mensagens de erro
            Static.show(title, Static.getErreursFromThrowable(e2));
        }
    }
}

6.4.7. Exclusão do conteúdo da tabela de produtos

O método [delete] exclui o conteúdo da tabela:


// exclusão de produtos
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // início da sessão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // esvaziamento da tabela [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // confirmação da transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // processando o finally
            doFinally(null, null, connexion);
        }
    }

Este exemplo utiliza transações. Uma transação permite agrupar ordens SQL que devem ser todas executadas com sucesso ou todas canceladas. Há quatro operações a serem consideradas:

  • início de uma transação: [connexion.setAutoCommit(false)];
  • conclusão bem-sucedida de uma transação: [connexion.commit()]. Nesse caso, todas as operações realizadas no BD durante a transação são validadas;
  • fim de uma transação com falha: [connexion.rollback()]. Nesse caso, todas as operações realizadas no BD durante a transação são canceladas;
  • retorno ao modo [auto-commit], que é o modo padrão do API JDBC: [connexion.setAutoCommit(true)]. Nesse modo, cada ordem SQL é objeto de uma transação. Assim, se forem feitas duas inserções e a segunda falhar:
    • no modo [AutoCommit=true], a primeira inserção permanece (ela foi validada pelo primeiro AutoCommit);
    • no modo [AutoCommit=false], a primeira inserção é cancelada;

Em nossos exemplos, sempre que ocorre uma exceção, cancelamos a transação no método [doCatchException]:


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // exibindo as mensagens de erro
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancelamento da transação
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // exibindo as mensagens de erro
            Static.show("Erreur lors de l'annulation de la transaction", Static.getErreursFromThrowable(e2));
        }
}

6.4.8. Criação do conteúdo da tabela de produtos

O método [insert] cria o conteúdo da tabela:


// adição de produtos
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // inicialização da conexão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // preenchimento da tabela
            ps = connexion.prepareStatement(insert);
            for (int i = 0; i < 10; i++) {
                // preparação
                int n = i + 1;
                ps.setInt(1, n);
                ps.setString(2, String.format("NOM%s", n));
                ps.setInt(3, n / 5 + 1);
                ps.setDouble(4, 100 * (1 + (double) i / 100));
                ps.setString(5, String.format("DESC%s", n));
                // execução
                ps.executeUpdate();
            }
            // confirmação da transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // tratando o finally
            doFinally(null, null, connexion);
        }
    }

6.4.9. Exibição do conteúdo da tabela de produtos

O método [select] exibe o conteúdo da tabela:


    // lista de produtos
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // abertura da conexão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // lendo a tabela [PRODUITS]
            ps = connexion.prepareStatement(select);
            rs = ps.executeQuery();
            System.out.println("Liste des produits : ");
            while (rs.next()) {
                System.out.println(new Produit(rs.getInt(1), rs.getString(2), rs.getInt(3), rs.getDouble(4), rs.getString(5)));
            }
            // confirmação da transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // processando o finally
            doFinally(null, null, connexion);
        }
}

6.4.10. Atualização do conteúdo da tabela

O método [update] atualiza determinados produtos:


// atualização de alguns produtos
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // abertura da conexão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // atualização da tabela
            ps = connexion.prepareStatement(update);
            // categoria 1
            ps.setInt(1, 1);
            // execução
            ps.executeUpdate();
            // confirmação da transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // tratando o finally
            doFinally(null, null, connexion);
        }
    }

6.4.11. Função da transação

O método [insert2] insere dois produtos com a mesma chave primária na tabela, o que não é permitido. Como estamos em uma transação, a primeira inserção será cancelada.


// adição de  2 produtos com as mesmas chaves primárias
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // abertura da conexão
            connexion = DriverManager.getConnection(url, user, passwd);
            // início da transação
            connexion.setAutoCommit(false);
            // adicionando 1 linha
            ps = connexion.prepareStatement(insert2);
            // execução
            ps.executeUpdate();
            // adiciona-se a mesma linha pela segunda vez, portanto com a mesma chave primária
            // a inserção deve falhar e nenhum dos dois elementos deve ser inserido devido à transação
            ps.executeUpdate();
            // confirma a transação
            connexion.commit();
            // retorno ao modo padrão
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // tratamos a exceção
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // processando o finally
            doFinally(null, null, connexion);
        }
    }

6.4.12. Resultados

A execução do método [main] produz os seguintes resultados:

Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":100.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":101.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":102.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":103.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Les erreurs suivantes se sont produites lors de l'ajout : 
- Duplicate entry '100' for key 'PRIMARY'
Liste des produits : 
{"id":1,"nom":"NOM1","categorie":1,"prix":110.0,"description":"DESC1"}
{"id":2,"nom":"NOM2","categorie":1,"prix":111.0,"description":"DESC2"}
{"id":3,"nom":"NOM3","categorie":1,"prix":112.0,"description":"DESC3"}
{"id":4,"nom":"NOM4","categorie":1,"prix":113.0,"description":"DESC4"}
{"id":5,"nom":"NOM5","categorie":2,"prix":104.0,"description":"DESC5"}
{"id":6,"nom":"NOM6","categorie":2,"prix":105.0,"description":"DESC6"}
{"id":7,"nom":"NOM7","categorie":2,"prix":106.0,"description":"DESC7"}
{"id":8,"nom":"NOM8","categorie":2,"prix":107.0,"description":"DESC8"}
{"id":9,"nom":"NOM9","categorie":2,"prix":108.0,"description":"DESC9"}
{"id":10,"nom":"NOM10","categorie":3,"prix":109.0,"description":"DESC10"}
Travail terminé

6.5. Utilização de uma fonte de dados do tipo [DataSource]

Vamos retomar o aplicativo anterior utilizando uma fonte de dados do tipo [javax.sql.DataSource]:

Image

Vamos utilizar uma fonte de dados implementada pela classe [org.apache.tomcat.jdbc.pool.DataSource]. Essa classe utiliza um pool de conexões, ou seja, um conjunto de conexões abertas:

  • quando o pool é instanciado, um determinado número de conexões é aberto com o banco de dados. Esse número é configurável;
  • quando o código Java abre uma conexão, ela é fornecida pelo pool;
  • quando o código Java fecha uma conexão, ela é devolvida ao pool;

No final das contas, as conexões são abertas apenas uma vez, o que melhora o desempenho do acesso ao banco de dados. A fonte de dados será definida em uma classe de configuração do Spring

6.5.1. O projeto Eclipse

  

O projeto é um projeto Maven definido pelo seguinte arquivo [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.jdbc</groupId>
    <artifactId>intro-jdbc-02</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <!-- MySQL -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <!-- biblioteca jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.5.1</version>
        </dependency>
        <!-- Spring -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.1.3.RELEASE</version>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
            <version>8.0.20</version>
        </dependency>
    </dependencies>
</project>
  • linhas 21-25: dependência do Spring;
  • linhas 27-31: dependência da biblioteca que fornece a fonte de dados;

A classe de configuração do Spring [AppConfig] é a seguinte:


package istia.st.jdbc;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    // constantes
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroJdbc";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String INSERT = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (?, ?, ?, ?, ?)";
    final static String DELETE = "DELETE FROM PRODUITS";
    final static String SELECT = "SELECT ID, NOM, CATEGORIE, PRIX, DESCRIPTION FROM PRODUITS";
    final static String UPDATE = "UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?";
    final static String INSERT2 = "INSERT INTO PRODUITS(ID, NOM, CATEGORIE, PRIX, DESCRIPTION) VALUES (100,'X',1,1,'x')";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    
    @Bean
    public DataSource dataSource() {
        // fonte de dados TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuração de acesso JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // uma conexão inicialmente aberta
        dataSource.setInitialSize(1);
        // resultado
        return dataSource;
    }
}
  • linhas 11-19: as constantes previamente definidas em [IntroJdbc01] foram migradas para [AppConfig];
  • linhas 31-34: o bean do Spring que define a fonte de dados;
  • linha 24: criação da fonte de dados, ainda não configurada;
  • linhas 26-29: as informações que permitem que a fonte de dados se conecte ao banco de dados;
  • linha 31: cria um pool com 1 conexão. Não precisamos de mais do que isso aqui. Nunca há várias conexões simultâneas;

6.5.2. A classe principal

A classe principal [IntroJdbc02] é a seguinte:


package istia.st.jdbc;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class IntroJdbc02 {
    // fonte de dados
    private static DataSource dataSource;

    public static void main(String[] args) {
        // recuperação do contexto Spring
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // recuperação da fonte de dados
        dataSource = ctx.getBean(DataSource.class);
        // esvaziando a tabela [PRODUITS]
        delete();
...
        // concluído
        ctx.close();
        System.out.println("Travail terminé");
    }

    // lista de produtos
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // abertura da conexão
            connexion = dataSource.getConnection();
            // início da transação
            connexion.setAutoCommit(false);
...
        } catch (SQLException e1) {
            // tratando a exceção
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // tratando o finally
            doFinally(null, null, connexion);
        }
    }
...
}
  • linha 14: a fonte de dados. Observe que ela é do tipo [javax.sql.DataSource], que é uma interface;
  • linha 18: instanciação dos objetos Spring;
  • linha 20: obtenção de uma referência à fonte de dados. Observe-se que, em nenhum momento, é mencionada a classe realmente utilizada. Assim, neste caso, nada sugere que se esteja utilizando uma implementação [TomcatJdbc];
  • linha 36: obtenção de uma conexão aberta;
  • o restante do código é idêntico ao da classe [IntroJdbc01];

6.6. Conclusion

Mais informações sobre o gerenciamento de bancos de dados podem ser encontradas no documento [Exploiter une base relationnelle avec l'écosystème Spring].