Skip to content

6. [Curso]: Introdução à API JDBC

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

6.1. Suporte

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

6.2. Arquitetura

A camada JDBC (Java Database Connectivity) é uma interface universal de acesso a bases de dados. Apresenta sempre a mesma interface à camada [DAO]. Se alterar o SGBD, basta alterar o controlador JDBC. A camada [DAO] permanece inalterada.

6.3. Passos para operar uma base de dados

Na arquitetura acima, a operação de uma base de dados através do programa de consola envolve os seguintes passos:

  1. carregar o controlador JDBC da base de dados;
  2. abrir uma ligação à base de dados;
  3. executar uma instrução SQL no banco de dados e processar os resultados da instrução SQL;
  4. fechar a ligação;

O Passo 1 é executado apenas uma vez. Os Passos 2 a 4 são executados repetidamente. Note-se que as ligações não ficam abertas; são encerradas assim que deixam de ser necessárias.

6.3.1. Passo 1 - Carregar o controlador JDBC na memória

O código


        // driver loading JDBC
        try {
            Class.forName(nom de la classe du pilote JDBC);
        } catch (ClassNotFoundException e1) {
             // handle the exception
}

O objetivo da operação na linha 3 é carregar o controlador JDBC da base de dados na memória. Esta operação só precisa de ser realizada uma vez. No entanto, repeti-la não causa um erro. A classe do controlador JDBC é procurada no caminho de classes do projeto. Portanto, no projeto Eclipse, o ficheiro [jar] que contém a classe do controlador JDBC deve ter sido incluído no caminho de classes do projeto.

6.3.2. Passo 2 - Abrir uma ligação

Assim que o controlador JDBC estiver instalado, instruímos-o a abrir uma ligação à base de dados:

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 {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
...
        } catch (SQLException e1) {
            // we handle the exception
            ...
        } finally {
         // close connection
         if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e2) {
                // handle the exception
                ...
            }
         }
}
  • Linhas 3–7: As classes que implementam a interface JDBC estão todas no pacote [java.sql]. Além disso, em caso de erro, todas lançam uma [SQLException] (linhas 19, 27). Esta exceção deriva da classe [Exception] e é uma chamada exceção verificada: deve utilizar um bloco try/catch para a tratar ou, em alternativa, optar por não a tratar e indicar que o método permite que a exceção se propague, adicionando [throws SQLException] à assinatura do método;
  • Linha 17: [DriverManager.getConnection] é um método estático que recebe três parâmetros:
    • [url]: o URL da base de dados. Trata-se de uma cadeia de caracteres que depende da base de dados utilizada. Para o MySQL, tem o formato [jdbc:mysql://localhost:3306/db_name];
    • [user]: o proprietário da ligação;
    • [passwd]: a palavra-passe do utilizador;
  • linhas 24–30: a ligação deve ser encerrada na cláusula [finally] para que seja encerrada independentemente de ocorrer ou não uma exceção.

6.3.3. Passo 3 - Executar instruções SQL [SELECT]

Assim que a ligação for estabelecida, os comandos SQL podem ser executados. A forma como os comandos de leitura [SELECT] são tratados difere daquela utilizada para operações de atualização [UPDATE, INSERT, DELETE]. Começaremos com os comandos SQL [SELECT]:

O código


Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // in read-only mode
            connexion.setReadOnly(true);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // we handle the exception
             doCatchException(connexion,e1);
        } finally {
            // we treat the 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 de leitura apenas (linha 10). Uma transação é uma sequência de instruções SQL que ou todas são bem-sucedidas ou todas falham. Assim, numa transação que contenha N instruções SQL, se a (I+1)ª instrução falhar, então as I instruções anteriores serão revertidas. Para uma operação de leitura, não é necessária uma transação. No entanto, a criação de uma transação de leitura apenas pode permitir que certos DBMS realizem determinadas otimizações;
  • Linha 12: utilização de um [PreparedStatement]. Um [PreparedStatement] tem normalmente parâmetros indicados pelo caractere ?. Aqui, não é o caso. Um [PreparedStatement] é uma instrução preparada pelo SGBD. Esta preparação tem um custo e é realizada apenas uma vez. Esta instrução preparada é então executada pelo SGBD com parâmetros reais que substituem os parâmetros formais ?. Note-se que é preferível especificar as colunas desejadas em vez de utilizar a notação * para recuperar todas as colunas. Ao especificar os nomes das colunas, os seus valores podem então ser recuperados com base na sua posição na instrução SELECT;
  • Linha 13: execução do [PreparedStatement]. É recuperado um objeto [ResultSet];

Um objeto [ResultSet] representa uma tabela, ou seja, um conjunto de linhas e colunas. A qualquer momento, temos acesso a apenas uma linha da tabela, chamada de linha atual. Quando o [ResultSet] é criado inicialmente, não há linha atual. Temos de realizar uma operação [ResultSet.next()] para a obter. A assinatura do método next é a seguinte:

    boolean next()

Este método tenta avançar para a linha seguinte do [ResultSet] e devolve true se for bem-sucedido, false caso contrário. Se for bem-sucedido, a linha seguinte torna-se a nova linha atual. A linha anterior é perdida e não pode ser recuperada.

A tabela [ResultSet] possui colunas denominadas labelCol1, labelCol2, ... conforme especificado 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] irá para uma coluna no [ResultSet] denominada [myId];
  • a coluna [NAME] irá para uma coluna no [ResultSet] chamada [myName];
  • ...

No exemplo acima, os identificadores [myCol] são chamados de rótulos de coluna. Sem esses rótulos, os nomes das colunas do [ResultSet] dependem do SGBD. Quando o [SELECT] opera numa única tabela, os rótulos de coluna serão, por predefinição, os nomes das colunas solicitadas pelo SELECT. O problema surge quando o [SELECT] opera em várias tabelas e essas tabelas contêm nomes de colunas idênticos, como no exemplo seguinte:

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

Supondo que a tabela [PRODUCTS] tenha uma chave estrangeira para a tabela [CATEGORIES], representada pela relação [PRODUCTS].CATEGORY_ID --> [CATEGORIES].ID, e que tanto a tabela [PRODUCTS] como a tabela [CATEGORIES] tenham um campo [NAME]. Neste caso, os nomes atribuídos no [ResultSet] às colunas [PRODUCTS.NAME] e [CATEGORIES.NAME] dependem do SGBD. Para garantir a portabilidade entre SGBDs, devem ser utilizados aqui os rótulos das colunas, pelo que escreveremos:


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

Para aceder aos vários campos da linha atual no [ResultSet], estão disponíveis os seguintes métodos:

Type getType("labelColi") 

para recuperar a coluna denominada "labelColi" da linha atual, ou seja, a coluna na instrução [SELECT] com essa etiqueta. Type refere-se ao tipo de dados do campo "labelColi". Podem ser utilizados os seguintes métodos [getType]: getInt, getLong, getString, getDouble, getFloat, getDate, ... Em vez de utilizar o nome da coluna, pode utilizar a sua posição na consulta [SELECT] executada:

Type getType(i) 

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

  • linhas 15–17: recuperação dos valores lidos da base de dados;
  • linha 19: a transação é validada (também conhecida como confirmada). Isto encerra a transação e liberta os recursos que o SGBD tinha alocado para ela;
  • linha 25: os recursos são libertados no bloco [finally]. Isto chama o seguinte método [doFinally]:

private void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        if (connexion != null) {
            try {
                // close connection
                connexion.close();
            } catch (SQLException e3) {
                 // handle the exception
            }
        }
    }
  • linhas 3-9: fechar o [ResultSet];
  • linhas 11–17: fechar o [PreparedStatement];
  • linhas 18–27: fechar a ligação;

Os encerramentos nas linhas 3–17 parecem redundantes, uma vez que a ligação é encerrada nas linhas 18–25. Na verdade, em alguns casos, não são redundantes, pelo que se recomenda 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 seguinte método [doCatchException]:

    private static void doCatchException(Connection connexion, Throwable th) {
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // handle the exception
        }
}
  • linhas 4–6: a transação é revertida. Isto encerra-a, e o SGBD pode libertar os recursos que lhe foram atribuídos;

6.3.4. Passo 3 - Emissão de instruções SQL [INSERT, UPDATE, DELETE]

As instruções SQL [INSERT, UPDATE, DELETE] são operações de atualização: modificam a base de dados, mas não devolvem quaisquer linhas. A única informação devolvida é o número de linhas afetadas pela operação de atualização.

O código


Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // ouverture connexion
            connexion = DriverManager.getConnection(url, user, passwd);
            // début transaction
            connexion.setAutoCommit(false);
            // en mode lecture / écriture
            connexion.setReadOnly(false);
            // on met à jour la table
            ps = connexion.prepareStatement("UPDATE PRODUITS SET PRIX=PRIX*1.1 WHERE CATEGORIE=?");
            // catégorie 1
            ps.setInt(1, 10);
            // exécution
            int nbLignes=ps.executeUpdate();
            // commit transaction
            connexion.commit();
        } catch (SQLException e1) {
            // on traite l'exception
            doCatchException(connexion, e1);
        } finally {
            // on traite le finally
            doFinally(null, ps, connexion);
        }
    }
  • linha 9: a ligação é utilizada para leitura e escrita;
  • linha 11: um [PreparedStatement] com 1 parâmetro (representado por ?). Pode haver vários parâmetros. São numerados a partir de 1;
  • linha 13: o seu valor é atribuído 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. Pode utilizar os métodos [setInt, setLong, setFloat, setDouble, setString, setDate, ...];
  • linha 15: é utilizado o método [executeUpdate], e não [executeQuery], que está reservado para instruções SELECT. O método devolve o número de linhas afetadas pela operação. Pode ser 0.
  • linha 17: a transação é confirmada;

6.3.5. Passo 4 - Fechar a ligação

Uma ligação deve ser encerrada o mais rapidamente possível num ambiente multiutilizador, porque um SGBD aceita um número limitado de ligações abertas. Nos exemplos anteriores, foi encerrada na cláusula [finally] das operações SQL, para que fosse encerrada independentemente de ter ocorrido ou não uma exceção.

6.4. Um projeto de exemplo

6.4.1. Suporte

A pasta [support / chap5] contém os projetos Eclipse para este capítulo [1, 2]. A pasta [database] contém o script SQL para criar a base de dados MySQL de exemplo para este capítulo [1, 3].

6.4.2. A base de dados utilizada

Os exemplos a seguir utilizam a seguinte base de dados MySQL:

 
  • [ID]: chave primária no modo AUTO_INCREMENT (se não for especificada nenhuma chave primária, o SGBD gera-a);
  • [NAME]: nome do produto — único;
  • [CATEGORY]: número da categoria;
  • [PRICE]: o seu preço;
  • [DESCRIPTION]: uma descrição do produto;

Iremos criá-la utilizando a ferramenta [WampServer] da seguinte forma [1-9]:

6.4.3. O Projeto Eclipse

  

O projeto é um projeto Maven definido pelo seguinte ficheiro [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 controlador JDBC para o SGBD MySQL5;
  • linhas 13–17: uma biblioteca capaz de lidar com JSON (JavaScript Object Notation) (ver Secção 22.6). Iremos utilizá-la para apresentar os produtos da base de dados no formato JSON;

6.4.4. A Classe Product

A classe [Product] é a seguinte:


package istia.st.jdbc;
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
 
public class Produit {
 
    // fields
    private int id;
    private String nom;
    private int categorie;
    private double prix;
    private String description;
 
    // manufacturers
    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 and setters
    ...
 
    // to String
    public String toString() {
        try {
            return new ObjectMapper().writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
}
  • Linha 34: Utilizamos a biblioteca JSON para apresentar a cadeia JSON do produto. Isto produz um resultado semelhante ao 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, mesmo que sejam adicionados ou removidos campos da classe, o seu método [toString] continua válido. Além disso, se os próprios campos forem objetos (listas, matrizes, dicionários, objetos de utilizador), as bibliotecas JSON podem, por sua vez, convertê-los em cadeias JSON;

6.4.5. A classe [Static]

A classe [Static] agrupa métodos que contêm código frequentemente utilizado 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) {
        // retrieve the list of exception error msgs
        List<String> erreurs = new ArrayList<String>();
        while (th != null) {
            // throwable error message
            erreurs.add(th.getMessage());
            // we move on to the cause of throwable
            th = th.getCause();
        }
        // result
        return erreurs;
    }
 
    public static void show(String title, List<String> messages){
        // title
        System.out.println(String.format("%s : ",title));
        // messages
        for(String message : messages){
            System.out.println(String.format("- %s",message));
        }
    }
}
  • linhas 8–19: devolve uma lista de erros encapsulados num objeto do tipo [Throwable], que é a superclasse da classe [Exception];
  • linhas 21-28: exibe uma lista de mensagens no ecrã;

Este código poderia estar na classe principal, pois é a única a utilizá-lo aqui. No entanto, estamos a considerar um cenário mais abrangente, em que outras classes possam necessitar deste código.

6.4.6. O esqueleto 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 {
 
    // constants
    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) {
        // loading the JDBC driver from MySQL
        try {
            Class.forName("com.mysql.jdbc.Driver");
        } catch (ClassNotFoundException e1) {
            doCatchException("Pilote JDBC introuvable", null, e1);
            return;
        }
        // empty table [PRODUITS]
        delete();
        // fill it
        insert();
        // we read it
        select();
        // update
        update();
        // display
        select();
        // insertion of two identical elements
        // insertion must fail and neither element is inserted because of the transaction
        insert2();
        // we check
        select();
        // finish
        System.out.println("Travail terminé");
    }
 
    // product list
    private static void select() {
...
    }
 
    // product deletion
    public static void delete() {
...
    }
 
    // add products
    public static void insert() {
...
    }
 
    // add 2 products
    public static void insert2() {
...
    }
 
    // product updates
    public static void update() {
..
    }
 
    private static void doFinally(ResultSet rs, PreparedStatement ps, Connection connexion) {
        // closure ResultSet
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException e1) {
 
            }
        }
        // closure [PreparedStatement]
        if (ps != null) {
            try {
                ps.close();
            } catch (SQLException e2) {
 
            }
        }
        // close connection
        if (connexion != null) {
            try {
                connexion.close();
            } catch (SQLException e3) {
                // display error msg
                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) {
        // display error msg
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            Static.show(title, Static.getErreursFromThrowable(e2));
        }
    }
}

6.4.7. Eliminar o conteúdo da tabela de produtos

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


// product deletion
    public static void delete() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // empty table [PRODUITS]
            ps = connexion.prepareStatement(delete);
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la suppression du contenu de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

Este exemplo utiliza transações. Uma transação permite agrupar instruções SQL que devem ser todas bem-sucedidas ou todas revertidas. Há quatro operações a ter em conta:

  • iniciar uma transação: [connection.setAutoCommit(false)];
  • concluir uma transação com sucesso: [connection.commit()]. Neste caso, todas as operações realizadas na base de dados durante a transação são confirmadas;
  • fim de uma transação com falha: [connection.rollback()]. Neste caso, todas as operações realizadas na base de dados durante a transação são revertidas;
  • Retornar ao modo [auto-commit], que é o modo padrão da API JDBC: [connection.setAutoCommit(true)]. Neste modo, cada instrução SQL faz parte de uma transação. Assim, se realizar duas inserções e a segunda falhar:
    • no modo [AutoCommit=true], a primeira inserção permanece (foi confirmada pelo primeiro AutoCommit);
    • no modo [AutoCommit=false], a primeira inserção é revertida;

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


    private static void doCatchException(String title, Connection connexion, Throwable th) {
        // display error msg
        Static.show(title, Static.getErreursFromThrowable(th));
        // cancel transaction
        try {
            if (connexion != null) {
                connexion.rollback();
            }
        } catch (SQLException e2) {
            // display error msg
            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:


// add products
    public static void insert() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // fill the table
            ps = connexion.prepareStatement(insert);
            for (int i = 0; i < 10; i++) {
                // preparation
                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));
                // execution
                ps.executeUpdate();
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la création du contenu de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

6.4.9. Exibir o conteúdo da tabela de produtos

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


    // product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // table [PRODUITS] is read
            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)));
            }
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
}

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

O método [update] atualiza determinados produtos:


// product updates
    public static void update() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // table is updated
            ps = connexion.prepareStatement(update);
            // category 1
            ps.setInt(1, 1);
            // execution
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la mise à jour du contenu de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

6.4.11. Papel 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 numa transação, a primeira inserção será revertida.


// add 2 pro ducts with the same primary keys
    public static void insert2() {
        Connection connexion = null;
        PreparedStatement ps = null;
        try {
            // opening connection
            connexion = DriverManager.getConnection(url, user, passwd);
            // start of transaction
            connexion.setAutoCommit(false);
            // add 1 line
            ps = connexion.prepareStatement(insert2);
            // execution
            ps.executeUpdate();
            // we add the same line a 2nd time, with the same primary key
            // the insertion must fail and neither of the two elements must be inserted because of the transaction
            ps.executeUpdate();
            // commit transaction
            connexion.commit();
            // return to default mode
            connexion.setAutoCommit(true);
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites lors de l'ajout", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }

6.4.12. Resultados

Os resultados da execução do método [main] são os seguintes:

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 [DataSource]

Vamos revisitar a aplicação anterior utilizando uma fonte de dados [javax.sql.DataSource]:

Image

Iremos utilizar uma fonte de dados implementada pela classe [org.apache.tomcat.jdbc.pool.DataSource]. Esta classe utiliza um conjunto de ligações, ou seja, um conjunto de ligações abertas:

  • quando o pool é instanciado, é aberto um determinado número de ligações à base de dados. Este número é configurável;
  • quando o código Java abre uma ligação, esta é fornecida pelo conjunto;
  • quando o código Java fecha uma ligação, esta é devolvida ao conjunto;

Em última análise, as ligações são abertas apenas uma vez, o que melhora o desempenho do acesso à base de dados. A fonte de dados será definida numa classe de configuração Spring

6.5.1. O projeto Eclipse

  

O projeto é um projeto Maven definido pelo seguinte ficheiro [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>
        <!-- library 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() {
        // source de données TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration accès JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        // une connexion ouverte initialement
        dataSource.setInitialSize(1);
        // résultat
        return dataSource;
    }
}
  • linhas 11–19: as constantes previamente definidas em [IntroJdbc01] foram movidas para [AppConfig];
  • linhas 31–34: o bean Spring que define a fonte de dados;
  • linha 24: criação da fonte de dados, que ainda não está configurada;
  • linhas 26–29: as informações que permitem que a fonte de dados se ligue à base de dados;
  • linha 31: cria um pool de 1 conexão. Não precisamos de mais do que isso aqui. Nunca há múltiplas 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 {
    // data source
    private static DataSource dataSource;
 
    public static void main(String[] args) {
        // spring context retrieval
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        // data source recovery
        dataSource = ctx.getBean(DataSource.class);
        // empty table [PRODUITS]
        delete();
...
        // finish
        ctx.close();
        System.out.println("Travail terminé");
    }
 
    // product list
    private static void select() {
        Connection connexion = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            // opening connection
            connexion = dataSource.getConnection();
            // start of transaction
            connexion.setAutoCommit(false);
...
        } catch (SQLException e1) {
            // we handle the exception
            doCatchException("Les erreurs suivantes se sont produites à la lecture de la table", connexion, e1);
        } finally {
            // we treat the finally
            doFinally(null, null, connexion);
        }
    }
...
}
  • linha 14: a fonte de dados. Note-se que é do tipo [javax.sql.DataSource], que é uma interface;
  • linha 18: instanciação de objetos Spring;
  • linha 20: obtenção de uma referência à fonte de dados. Note-se que a classe efetivamente utilizada nunca é mencionada. Assim, aqui, nada sugere que esteja a ser utilizada uma implementação [TomcatJdbc];
  • linha 36: obtenção de uma ligação aberta;
  • o resto do código é idêntico ao da classe [IntroJdbc01];

6.6. Conclusão

Mais informações sobre gestão de bases de dados podem ser encontradas no documento [Trabalhar com uma Base de Dados Relacional Utilizando o Ecossistema Spring].