Introdução ao JDBC

Introdução ao JDBC

*1. Visão geral *

Neste artigo, veremos o JDBC (Java Database Connectivity), uma API para conectar e executar consultas em um banco de dados.

O JDBC pode trabalhar com qualquer banco de dados, desde que os drivers adequados sejam fornecidos.

===* 2. Drivers JDBC *

Um driver JDBC é uma implementação da API JDBC usada para conectar-se a um tipo específico de banco de dados. Existem vários tipos de drivers JDBC:

  • Tipo 1 - contém um mapeamento para outra API de acesso a dados; um exemplo disso é o driver JDBC-ODBC

  • Tipo 2 - é uma implementação que usa bibliotecas do lado do cliente do banco de dados de destino; também chamado de driver de API nativa

  • Tipo 3 - usa middleware para converter chamadas JDBC em chamadas específicas do banco de dados; também conhecido como driver de protocolo de rede *Tipo 4 - conecte-se diretamente a um banco de dados convertendo chamadas JDBC em chamadas específicas do banco de dados; conhecidos como drivers de protocolo de banco de dados ou drivers thin,

O tipo mais usado é o tipo 4, pois possui a vantagem de ser* independente da plataforma *. A conexão direta a um servidor de banco de dados fornece melhor desempenho em comparação com outros tipos. A desvantagem desse tipo de driver é que ele é específico do banco de dados - dado que cada banco de dados possui seu próprio protocolo específico.

*3. Conectando a um banco de dados *

Para conectar-se a um banco de dados, basta inicializar o driver e abrir uma conexão com o banco de dados.

====* 3.1 Registrando o driver *

Para o nosso exemplo, usaremos um driver de protocolo de banco de dados tipo 4.

Como estamos usando um banco de dados MySQL, precisamos do https://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%22mysql-connector-java%22%20AND%20g%3A%22mysql % 22 [mysql-connector-java] dependência:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>6.0.6</version>
</dependency>

Em seguida, vamos registrar o driver usando o método _Class.forName () _, que carrega dinamicamente a classe do driver:

Class.forName("com.mysql.cj.jdbc.Driver");

====* 3.2 Criando a conexão *

Para abrir uma conexão, podemos usar o método getConnection () _ da classe _DriverManager. Este método requer um parâmetro String da URL de conexão:

Connection con = DriverManager
  .getConnection("jdbc:mysql://localhost:3306/myDb", "user1", "pass");

A sintaxe da URL de conexão depende do tipo de banco de dados usado. Vamos dar uma olhada em alguns exemplos:

jdbc:mysql://localhost:3306/myDb?user=user1&password=pass
jdbc:postgresql://localhost/myDb
jdbc:hsqldb:mem:myDb

Para conectar-se ao banco de dados myDb especificado, teremos que criar o banco de dados e um usuário e adicionar conceder o acesso necessário:

CREATE DATABASE myDb;
CREATE USER 'user1' IDENTIFIED BY 'pass';
GRANT ALL on myDb. *TO 'user1';

===* 4. Executando instruções SQL *

Ao enviar instruções SQL para o banco de dados, podemos usar instâncias do tipo Statement, PreparedStatement ou CallableStatement. Estes são obtidos usando o objeto Connection.

====* 4.1 Declaração*

A interface Statement contém as funções essenciais para executar comandos SQL.

Primeiro, vamos criar um objeto Statement:

Statement stmt = con.createStatement();

A execução de instruções SQL pode ser feita através do uso de três métodos:

  • _executeQuery () _ para instruções SELECT

  • _executeUpdate () _ para atualizar os dados ou a estrutura do banco de dados

  • _execute () _ pode ser usado nos dois casos acima, quando o resultado é desconhecido

Vamos usar o método execute () _ para adicionar uma tabela _students ao nosso banco de dados:

String tableSql = "CREATE TABLE IF NOT EXISTS employees"
  + "(emp_id int PRIMARY KEY AUTO_INCREMENT, name varchar(30),"
  + "position varchar(30), salary double)";
stmt.execute(tableSql);
*Se o método _execute () _ for usado para atualizar os dados, o método _stmt.getUpdateCount () _ retornará o número de linhas afetadas.*

Se o resultado for 0, nenhuma linha foi afetada ou foi um comando de atualização da estrutura do banco de dados.

Se o valor for -1, o comando foi uma consulta SELECT. O resultado pode ser obtido usando _stmt.getResultSet () _.

Em seguida, vamos adicionar um registro à nossa tabela usando o método _executeUpdate () _:

String insertSql = "INSERT INTO employees(name, position, salary)"
  + " VALUES('john', 'developer', 2000)";
stmt.executeUpdate(insertSql);

O método retorna o número de linhas afetadas para um comando que atualiza linhas ou 0 para um comando que atualiza a estrutura do banco de dados.

Podemos recuperar os registros da tabela usando o método executeQuery () _ que retorna um objeto do tipo _ResultSet:

String selectSql = "SELECT *FROM employees";
ResultSet resultSet = stmt.executeQuery(selectSql);

====* 4.2 Declaração preparada *

Os objetos PreparedStatement contêm sequências SQL pré-compiladas. Eles podem ter um ou mais parâmetros indicados por um ponto de interrogação.

Vamos criar um PreparedStatement que atualiza os registros na tabela employees com base nos parâmetros fornecidos:

String updatePositionSql = "UPDATE employees SET position=? WHERE emp_id=?";
PreparedStatement pstmt = con.prepareStatement(updatePositionSql);

Para adicionar parâmetros ao PreparedStatement, podemos usar setters simples - _setX () _ - em que X é o tipo do parâmetro e os argumentos do método são a ordem e o valor do parâmetro:

pstmt.setString(1, "lead developer");
pstmt.setInt(2, 1);

A instrução é executada com um dos mesmos três métodos descritos anteriormente: executeQuery (), executeUpdate (), execute () _ sem o parâmetro SQL _String:

int rowsAffected = pstmt.executeUpdate();

====* 4.3 CallableStatement *

A interface CallableStatement permite chamar procedimentos armazenados.

Para criar um objeto CallableStatement, podemos usar o método prepareCall () _ de _Connection:

String preparedSql = "{call insertEmployee(?,?,?,?)}";
CallableStatement cstmt = con.prepareCall(preparedSql);

A configuração dos valores dos parâmetros de entrada para o procedimento armazenado é feita como na interface PreparedStatement, usando os métodos _setX () _:

cstmt.setString(2, "ana");
cstmt.setString(3, "tester");
cstmt.setDouble(4, 2000);

Se o procedimento armazenado tiver parâmetros de saída, precisamos adicioná-los usando o método _registerOutParameter () _:

cstmt.registerOutParameter(1, Types.INTEGER);

Então, vamos executar a instrução e recuperar o valor retornado usando um método _getX () _ correspondente:

cstmt.execute();
int new_id = cstmt.getInt(1);

Por exemplo, para funcionar, precisamos criar o procedimento armazenado em nosso banco de dados MySql:

delimiter//
CREATE PROCEDURE insertEmployee(OUT emp_id int,
  IN emp_name varchar(30), IN position varchar(30), IN salary double)
BEGIN
INSERT INTO employees(name, position,salary) VALUES (emp_name,position,salary);
SET emp_id = LAST_INSERT_ID();
END//
delimiter ;

O procedimento insertEmployee acima inserirá um novo registro na tabela employees usando os parâmetros fornecidos e retornará a identificação do novo registro no parâmetro emp_id out.

Para poder executar um procedimento armazenado a partir de Java, o usuário da conexão precisa ter acesso aos metadados do procedimento armazenado. Isso pode ser conseguido concedendo direitos ao usuário em todos os procedimentos armazenados em todos os bancos de dados:

GRANT ALL ON mysql.proc TO 'user1';

Como alternativa, podemos abrir a conexão com a propriedade noAccessToProcedureBodies definida como true:

con = DriverManager.getConnection(
  "jdbc:mysql://localhost:3306/myDb?noAccessToProcedureBodies=true",
  "user1", "pass");

Isso informará a API JDBC que o usuário não tem direitos para ler os metadados do procedimento, para que ele crie todos os parâmetros como parâmetros INOUT String.

===* 5. Analisando resultados da consulta *

Depois de executar uma consulta, o resultado é representado por um objeto ResultSet, com uma estrutura semelhante a uma tabela, com linhas e colunas.

====* 5.1. Interface ResultSet *

O ResultSet usa o método _next () _ para ir para a próxima linha.

Vamos primeiro criar uma classe Employee para armazenar nossos registros recuperados:

public class Employee {
    private int id;
    private String name;
    private String position;
    private double salary;

   //standard constructor, getters, setters
}

Em seguida, vamos percorrer o ResultSet e criar um objeto Employee para cada registro:

String selectSql = "SELECT* FROM employees";
ResultSet resultSet = stmt.executeQuery(selectSql);

List<Employee> employees = new ArrayList<>();

while (resultSet.next()) {
    Employee emp = new Employee();
    emp.setId(resultSet.getInt("emp_id"));
    emp.setName(resultSet.getString("name"));
    emp.setPosition(resultSet.getString("position"));
    emp.setSalary(resultSet.getDouble("salary"));
    employees.add(emp);
}

A recuperação do valor para cada célula da tabela pode ser feita usando métodos do tipo getX () em que X representa o tipo dos dados da célula.

Os métodos getX () _ podem ser usados ​​com um parâmetro _int que representa a ordem da célula ou um parâmetro String que representa o nome da coluna. A última opção é preferível caso alteremos a ordem das colunas na consulta.

5.2 ResultSet atualizável

Implicitamente, um objeto ResultSet só pode ser deslocado para frente e não pode ser modificado.

Se quisermos usar o ResultSet para atualizar os dados e percorrê-los nas duas direções, precisamos criar o objeto Statement com parâmetros adicionais:

stmt = con.createStatement(
  ResultSet.TYPE_SCROLL_INSENSITIVE,
  ResultSet.CONCUR_UPDATABLE
);

Para navegar neste tipo de ResultSet, podemos usar um dos métodos:

  • first (), last (), beforeFirst (), beforeLast () _ - para mover para a primeira ou a última linha de um _ResultSet ou para a linha antes desses

  • next (), previous () _ - para navegar para frente e para trás no _ResultSet

  • getRow () – para obter o número da linha atual

  • _moveToInsertRow (), moveToCurrentRow () _ - para mover para uma nova linha vazia para inserir e retornar à atual, se estiver em uma nova linha

  • absolute (int row) – para ir para a linha especificada

  • _relative (int nrRows) _ - para mover o cursor pelo número especificado de linhas

A atualização do ResultSet pode ser feita usando métodos com o formato updateX () _ em que X é o tipo de dados da célula. Esses métodos atualizam apenas o objeto _ResultSet e não as tabelas do banco de dados.

Para persistir as alterações ResultSet no banco de dados, precisamos usar ainda um dos métodos:

  • _updateRow () _ - para persistir as alterações na linha atual no banco de dados

  • _insertRow (), deleteRow () _ - para adicionar uma nova linha ou excluir a atual do banco de dados

  • refreshRow () _ - para atualizar o _ResultSet com quaisquer alterações no banco de dados *_cancelRowUpdates () _ - para cancelar as alterações feitas na linha atual

Vamos dar uma olhada em um exemplo de uso de alguns desses métodos, atualizando dados na tabela employee’s:

Statement updatableStmt = con.createStatement(
  ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet updatableResultSet = updatableStmt.executeQuery(selectSql);

updatableResultSet.moveToInsertRow();
updatableResultSet.updateString("name", "mark");
updatableResultSet.updateString("position", "analyst");
updatableResultSet.updateDouble("salary", 2000);
updatableResultSet.insertRow();

===* 6. Analisando Metadados *

A API JDBC permite procurar informações sobre o banco de dados, chamadas metadados.

====* 6.1 DatabaseMetadata *

A interface DatabaseMetadata pode ser usada para obter informações gerais sobre o banco de dados, como tabelas, procedimentos armazenados ou dialeto SQL.

Vamos dar uma olhada rápida em como podemos recuperar informações nas tabelas do banco de dados:

DatabaseMetaData dbmd = con.getMetaData();
ResultSet tablesResultSet = dbmd.getTables(null, null, "%", null);
while (tablesResultSet.next()) {
    LOG.info(tablesResultSet.getString("TABLE_NAME"));
}

====* 6.2 ResultSetMetadata *

Essa interface pode ser usada para encontrar informações sobre um determinado ResultSet, como o número e o nome de suas colunas:

ResultSetMetaData rsmd = rs.getMetaData();
int nrColumns = rsmd.getColumnCount();

IntStream.range(1, nrColumns).forEach(i -> {
    try {
        LOG.info(rsmd.getColumnName(i));
    } catch (SQLException e) {
        e.printStackTrace();
    }
});

===* 7. Manuseio de transações *

Por padrão, cada instrução SQL é confirmada logo após a conclusão. No entanto, também é possível* controlar transações programaticamente *.

Isso pode ser necessário nos casos em que desejamos preservar a consistência dos dados, por exemplo, quando queremos apenas confirmar uma transação se uma anterior tiver sido concluída com êxito.

Primeiro, precisamos definir a propriedade autoCommit de Connection como false e, em seguida, usar os métodos _commit () _ e _rollback () _ para controlar a transação.

Vamos adicionar uma segunda instrução de atualização para a coluna salary após a atualização da coluna position do funcionário e agrupar as duas em uma transação. Dessa forma, o salário será atualizado apenas se a posição tiver sido atualizada com êxito:

String updatePositionSql = "UPDATE employees SET position=? WHERE emp_id=?";
PreparedStatement pstmt = con.prepareStatement(updatePositionSql);
pstmt.setString(1, "lead developer");
pstmt.setInt(2, 1);

String updateSalarySql = "UPDATE employees SET salary=? WHERE emp_id=?";
PreparedStatement pstmt2 = con.prepareStatement(updateSalarySql);
pstmt.setDouble(1, 3000);
pstmt.setInt(2, 1);

boolean autoCommit = con.getAutoCommit();
try {
    con.setAutoCommit(false);
    pstmt.executeUpdate();
    pstmt2.executeUpdate();
    con.commit();
} catch (SQLException exc) {
    con.rollback();
} finally {
    con.setAutoCommit(autoCommit);
}

8. Fechando a conexão

Quando não estamos mais usando, é necessário fechar a conexão para liberar recursos do banco de dados.

Isso pode ser feito usando a API _close () _:

con.close();

9. Conclusão

Neste tutorial, vimos os conceitos básicos de trabalho com a API JDBC.

Como sempre, o código fonte completo dos exemplos pode ser encontrado over no GitHub.