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.