Java Database Connectivity, ou JDBC, é um dos pacotes mais usados
frequentemente em todo o JDK e mesmo assim poucos desenvolvedores de
Java aproveitam sua capacidade total ou mais atualizada. Neste artigo, confira uma introdução aos recursos mais novos de JDBC, como ResultSets com rolagem e atualização dinâmicas, Rowsets
que podem funcionar com ou sem uma conexão com o banco de dados, além de
atualizações com o lote que podem executar várias instruções SQL em um
percurso rápido pela rede.
Atualmente, muitos desenvolvedores de Java conhecem a API Java
Database Connectivity (JDBC) por meio de uma plataforma de acesso a
dados, como Hibernate ou Spring. Mas o JDBC é mais do que um reprodutor
de plano de fundo na conectividade do banco de dados. Quanto mais você
souber sobre isso, mais eficientes serão suas interações com RDBMS.
Nesta área da série 5 coisas,
vou demonstrar vários dos novos recursos introduzidos entre JDBC 2.0 e
JDBC 4.0. Projetados visando os desafios de desenvolvimento de software
moderno, esses recursos suportam a escalabilidade do aplicativo e a
produtividade do desenvolvedor dois dos desafios comuns enfrentados
pelos desenvolvedores de Java atualmente.
1. Funções escalares
Diversas implementações de RDBMS oferecem suporte irregular para
recursos de SQL e/ou de valor agregado projetados para a facilidade do
desenvolvedor. É bem conhecido, por exemplo, que a SQL fornece uma
operação escalar, COUNT(), para retornar o número de linhas que atende um critério de filtragem de SQL específico (ou seja, o predicado WHERE
).
Mas, além disso, a tentativa de modificar os valores retornados por
SQL pode ser complicada e a tentativa de obter data e hora atuais do
banco de dados pode enlouquecer (e possivelmente estressar também) até
mesmo o desenvolvedor de JDBC mais paciente. Para isso, a especificação JDBC fornece um grau de
isolamento/adaptação contra diferentes implementações de RDBMS, por meio
de funções escalares.
A especificação de JDBC inclui uma lista de
operações suportadas que os drivers JDBC reconhecem e adaptam conforme
necessário para a respectiva implementação de banco de dados específica.
Portanto, para um banco de dados que suportava o retorno à data e/ou
hora atuais, pode ser tão simples como a Listagem 1:
Listagem 1. Que horas são?
Connection conn = ...; // get it from someplace
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("{fn CURRENT_DATE()}");
A lista completa de funções escalares reconhecidas pela API JDBC é fornecida no apêndice da especificação JDBC,
mas a lista completa pode não ser suportada por um determinado driver ou banco de dados. É possível usar o objeto DatabaseMetaData retornado de
Connection para obter as funções suportadas por uma determinada implementação de JDBC, conforme mostrado na Listagem 2:
Listagem 2. O que você pode fazer por mim?
Connection conn = ...; // get it from someplace
DatabaseMetaData dbmd = conn.getMetaData();
A lista de funções escalares é um String retornado de vários métodos DatabaseMetaData. Por exemplo, todos os escalares numéricos são listados via chamada getNumericFunctions(). Execute um String.split() no resultado e aparecerá uma lista equals()-testable instantânea.
2. ResultSets roláveis
É um procedimento muito comum em JDBC para criar um objeto Connection (ou obter um existente) e usá-lo para criar um Statement.. O Statement, que está sendo alimentado por um SQL SELECT, retorna um ResultSet. O ResultSet é então alimentado por meio de um loop while (não diferente de um Iterator) até que ResultSet informe que está vazio, com o corpo do loop extraindo uma coluna por vez da esquerda para a direita.
Toda essa operação é tão comum que ela se tornou quase consagrada: é
executada dessa maneira simplesmente porque é como é feita. Ora, ela é
completamente desnecessária.
Introduzindo o ResultSet rolável
Muitos desenvolvedores não têm ciência do fato de o JDBC ter sido
consideravelmente aperfeiçoado com o passar dos anos, ainda que os
aprimoramentos sejam refletidos nos novos números de versão e releases. O
primeiro maior aprimoramento, o JDBC 2.0, ocorreu perto do lançamento
do JDK 1.2. Quando este artigo foi escrito, o JDBC estava na versão 4.0.
Um dos aprimoramentos interessantes (embora frequentemente ignorado) para o JDBC 2.0 é a capacidade de “rolar” pelo ResultSet,
ou seja, podemos ir para frente ou para trás, ou para ambas as direções
ao mesmo tempo, conforme necessário. Para isso é necessário um avanço,
mas a chamada de JDBC deve indicar que ela deseja um ResultSet no momento em que Statement é criado.
Se o driver JDBC subjacente suportar a rolagem, o ResultSet rolável será retornado desse Statement,
mas é melhor entender se o driver suporta a capacidade de rolagem antes
de solicitá-la. Você pode perguntar sobre a rolagem por meio do objeto DatabaseMetaData, que pode ser obtido de qualquer Connection, conforme descrito anteriormente.
Assim que você tiver um objeto DatabaseMetaData, uma chamada para getJDBCMajorVersion()
determinará se o driver suporta pelo menos a especificação JDBC 2.0.
É
claro que se um driver não condiz com seu nível de suporte para uma
determinada especificação, então para reproduzi-lo particularmente com
segurança, chame o método supportsResultSetType() com o tipo ResultSet desejado. (É uma constante na classe do ResultSet. Falaremos sobre os valores de cada logo a seguir.)
Listagem 3. Você consegue rolar?
int JDBCVersion = dbmd.getJDBCMajorVersion();
boolean srs = dbmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);
if (JDBCVersion > 2 || srs == true)
{
// scroll, baby, scroll!
}
Verificando o tipo ResultSet
Se você suspeita de que um driver pode realmente não suportar ResultSets roláveis, apesar do que ele informa no DatabaseMetaData, é possível verificar o tipo ResultSet chamando-se getType(). É claro que, se você possuir essa suspeita, poderá não confiar no valor de retorno de getType() também. É suficiente dizer que, se getType() não condiz com o ResultSet retornado, você poderá realmente ser pego em uma armadilha.
Solicitando um ResultSet rolável
Supondo-se que seu driver informe que sim (se ele não fizer isso, será
necessário um novo driver ou banco de dados), você poderá solicitar um ResultSet rolável passando dois parâmetros para a chamada de Connection.createStatement(), mostrada na Listagem 4:
Listagem 4. Eu quero rolar!
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_READ_ONLY);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
Você deve tomar cuidado especial ao chamar createStatement() porque seu primeiro e segundo parâmetros são intS. (Desaprove o fato de não termos enumerado os tipos até o Java 5!) Qualquer valor int (incluindo o valor constante errado) funcionará com o createStatement().
O primeiro parâmetro, que indica a “capacidade de rolagem” desejada no ResultSet, pode ser um dos três valores aceitos:
- ResultSet.TYPE_FORWARD_ONLY: Esse é o padrão, o estilo de cursor firehose que conhecemos e adoramos.
- ResultSet.TYPE_SCROLL_INSENSITIVE: Esse ResultSet permite a iteração para trás e para frente, mas se os dados no banco de dados forem alterados, o ResultSet não a refletirá. Esse ResultSet rolável é provavelmente o tipo desejado mais comum.
- ResultSet.TYPE_SCROLL_SENSITIVE: O ResultSet
criado não só permitirá a iteração bidirecional, como também fornecerá
uma visualização “ativa” dos dados no banco de dados quando forem
alterados.
O segundo parâmetro é discutido na próxima dica, espere mais um pouco.
Rolagem direcional
Assim que você tiver obtido um ResultSet do Statement, rolar para trás nele é apenas uma questão de chamada do previous(), que retrocede uma linha em vez de avançar, como next()
faria. Ou é possível chamar first() para retornar ao início do ResultSet, ou chamar last() para ir para o final do ResultSet, ou … bem, você entendeu.
Os métodos relative() e absolute()
também podem ser úteis: o primeiro move o número especificado de
linhas (para frente se o valor for positivo e para trás se o valor for
negativo), e o segundo move para a linha especificada no ResultSet independentemente da posição do cursor. É claro que o número da linha atual está disponível via getRow().
Se você planeja realizar muita rolagem em uma direção específica, pode ajudar o ResultSet especificando essa direção, chamando setFetchDirection(). (Um ResultSet funcionará independentemente da direção da rolagem, mas um conhecimento antecipado permite otimizar a recuperação dos dados.)
3. ResultSets atualizáveis
O JDBC não só suporta ResultSets, com também suporta atualizações do ResultSets
no local. Isso significa que em vez de criar uma nova instrução SQL
para alterar os valores armazenados atualmente no banco de dados, é
possível simplesmente modificar o valor mantido no ResultSet, e ele será enviado automaticamente para o banco de dados para a coluna dessa linha.
A solicitação de um ResultSet é semelhante ao processo envolvido na solicitação de um ResultSet rolável. Na verdade, é onde você usará o segundo parâmetro para o createStatement(). Em vez de especificar ResultSet.CONCUR_READ_ONLY
para o segundo parâmetro, envie ResultSet.CONCUR_UPDATEABLE,
conforme mostrado na Listagem 5:
Listagem 5. Eu gostaria de um ResultSet atualizável
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
Supondo que seu driver suporte cursores atualizáveis (esse é outro
recurso da especificação JDBC 2.0, que a maioria dos bancos de dados do
“mundo real” suportará), é possível atualizar qualquer valor em um ResultSet navegando nessa linha e chamando um dos métodos update…() nele (mostrado na Listagem 6).
Como os métodos get…() em ResultSet, update…() é sobrecarregado para o tipo de coluna real no ResultSet. Portanto, altere a coluna de ponto flutuante chamada
“PRICE”, chame updateFloat(“PRICE”).
Entretanto, esse procedimento apenas atualiza o valor no ResultSet. Para enviar o valor para o banco de dados retrocedendo-o, chame updateRow(). Se o usuário mudar de ideia sobre a mudança no preço, uma chamada para cancelRowUpdates() eliminará todas as atualizações pendentes.
Listagem 6. Uma maneira melhor
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS =
stmt.executeQuery("SELECT * FROM lineitem WHERE id=1");
scrollingRS.first();
scrollingRS.udpateFloat("PRICE", 121.45f);
// ...
if (userSaidOK)
scrollingRS.updateRow();
else
scrollingRS.cancelRowUpdates();
O JDBC 2.0 suporta mais do que apenas atualizações. Se o usuário
quiser incluir uma linha completamente nova, em vez de criar um novo Statement e executar um INSERT, simplesmente chame moveToInsertRow(), chame update…() para cada coluna e, em seguida, chame insertRow() para concluir o trabalho.
Se um valor de coluna não for especificado, será assumido como um SQL NULL (que pode acionar um SQLException se o esquema do banco de dados não permitir NULLs para essa coluna). Naturalmente, se ResultSet suportar a atualização em uma linha, ele deverá também suportar a exclusão de uma via deleteRow().
Ah, e antes que eu esqueça, toda essa capacidade de rolagem e de atualização se aplica igualmente a PreparedStatement (passando esses parâmetros para o método prepareStatement() ), que é infinitamente preferencial a um Statement devido ao perigo constante de ataques de injeção de SQL.
4. Rowsets
Se toda essa funcionalidade esteve no JDBC na melhor parte de uma
década, por que a maioria dos desenvolvedores ainda está presa ao
processo de rolar para frente os ResultSets e o acesso desconectado?
A escalabilidade é a principal culpada. Manter as conexões com o
banco de dados em um nível mínimo é a chave para suportar o grande
número de usuários que a Internet pode levar ao Web site de uma empresa.
Como a rolagem e/ou a atualização de ResultSets geralmente requer uma conexão de rede aberta, muitos desenvolvedores não (ou não poderão) usá-los.
Felizmente o JDBC 3.0 introduziu uma alternativa que permite que você faça as mesmas coisas que faria com um ResultSet, sem necessariamente precisar manter aberta a conexão com o banco de dados.
No conceito, um Rowset é essencialmente um ResultSet, mas um que permite um modelo conectado ou desconectado. Tudo que você precisa é criar um Rowset, apontar para ele em um ResultSet, e quando ele concluir o próprio preenchimento, use-o como você usaria um ResultSet, mostrado na Listagem 7:
Listagem 7. Rowset substitui ResultSet
Statement stmt = con.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE,
ResultSet.CONCUR_UPDATEABLE);
ResultSet scrollingRS = stmt.executeQuery("SELECT * FROM whatever");
if (wantsConnected)
JdbcRowSet rs = new JdbcRowSet(scrollingRS); // connected
else
CachedRowSet crs = new CachedRowSet(scrollingRS); disconnected
O JDBC é fornecido com cinco “implementações” (ou seja, interfaces estendidas) da interface de Rowset. JdbcRowSet é uma implementação de
Rowset conectada. As quatro restantes são desconectadas:
- CachedRowSet é apenas um Rowset desconectado .
- WebRowSet é uma subclasse de CachedRowSet que sabe como transformar seus resultados em XML e retornar.
- JoinRowSet é um WebRowSet que também sabe como formar o equivalente a um SQL JOIN sem ter que se conectar novamente ao banco de dados.
- FilteredRowSet é um WebRowSet que também sabe como filtrar adicionalmente os dados retornados sem ter que se conectar novamente ao banco de dados.
Rowsets são JavaBeans integrais, ou seja, eles suportam eventos do tipo listener, portanto, qualquer modificação no Rowset pode ser capturada, examinada e influenciada, se desejado.
Na verdade, Rowset pode até mesmo gerenciar a ação completa em relação ao banco de dados se ele tiver o conjunto de propriedades Username, Password, URL e DatasourceName (o que significa que ele criará uma conexão usando DriverManager.getConnection()) ou seu conjunto de propriedades Datasource (que provavelmente foi obtido por meio de JNDI).
Você então especifica o SQL para executar na propriedade Command, chamar execute() e começar a trabalhar com os resultados sem a necessidade de mais nenhuma ação. As implementações de Rowset geralmente são fornecidas pelo driver JDBC de modo que o nome real e/ou o pacote dependa de o que o driver JDBC usará.
Essas implementações têm sido parte de uma distribuição padrão desde o Java 5, portanto, você deve ser capaz de criar o …RowsetImpl()
e seguir em frente. (No caso improvável de seu driver não fornecer uma,
a Sun oferece uma implementação de referência. Consulte a seção Recursos ao final do artigo para obter o link.)
5. Atualizações de lote
Apesar da utilidade, os Rowsets às vezes simplesmente
não atendem às suas necessidades, e talvez seja necessário retornar para
escrever diretamente as instruções SQL.
Nessas situações,
particularmente quando estiver realizando grande volume de trabalho,
talvez você aprecie a capacidade de executar atualizações de lote,
executando mais de uma instrução SQL em relação ao banco de dados como
parte de roundtrip na rede.
Para determinar se o driver JDBC suporta atualizações de lote, uma chamada rápida para o DatabaseMetaData.supportsBatchUpdates()
gera um operador booleano contendo as informações. Supondo-se que as
atualizações de lote sejam suportadas (indicadas por qualquer não SELECT), enfileire uma e libere-a de uma vez, como na Listagem 8:
Listagem 8. Deixe o banco de dados executar a tarefa!
conn.setAutoCommit(false);
PreparedStatement pstmt = conn.prepareStatement("INSERT INTO lineitems VALUES(?,?,?,?)");
pstmt.setInt(1, 1);
pstmt.setString(2, "52919-49278");
pstmt.setFloat(3, 49.99);
pstmt.setBoolean(4, true);
pstmt.addBatch();
// rinse, lather, repeat
int[] updateCount = pstmt.executeBatch();
conn.commit();
conn.setAutoCommit(true);
A chamada para setAutoCommit() é necessária porque, por
padrão, o driver tentará confirmar cada instrução que for alimentada.
Além disso, o restante do código é bem claro: executar o SQL comum com Statement ou PreparedStatement, mas em vez de chamar execute(), chame executeBatch(), que coloca a chamada em fila em vez de enviá-la imediatamente.
Quando todas essas instruções estiverem prontas para serem executadas, ative todas no banco de dados com executeBatch(), que retorna um array de valores de número inteiro, cada um dos quais mantém o mesmo resultado como se executeUpdate() tivesse sido usado.
No caso de uma instrução no lote falhar, se o driver não suportar atualizações de lote, ou se uma instrução no lote retornar um ResultSet, o driver lançará um BatchUpdateException.
Em alguns casos, o driver poderá tentar continuar a executar as
instruções depois de uma exceção ter sido lançada.
A especificação JDBC
não determina um comportamento específico, portanto, você é aconselhado a
testar com seu driver antecipadamente de modo que saiba exatamente como
ele se comporta. (Mas você executará testes de unidade a fim de
descobrir o erro bem antes de ele se tornar um problema, certo?)
Conclusão
Como uma base do desenvolvimento Java, a API JDBC é algo que todo
desenvolvedor de Java deveria conhecer muito bem. O curioso é que a
maioria dos desenvolvedores não acompanhou os aprimoramentos da API com o
passar dos anos e não aprendeu os truques de economia de tempo
descritos neste artigo.
A decisão de usar os recursos mais recentes de JDBC é sua, é claro.
Um aspecto importante a ser considerado será a escalabilidade do sistema
em que você está trabalhando.
Quanto maior a necessidade de escalar,
mais você será forçado a usar o banco de dados e, portanto, maior será a
necessidade de reduzir o tráfego de rede em relação a ele. Rowsets, chamadas escalares e atualizações de lotes serão seus amigos. Caso contrário, tente os ResultSets roláveis e atualizáveis (que não consomem tanta memória quanto Rowsets) e meça a ocorrência de escalabilidade. É provável que não seja tão ruim quanto você espera.
Acompanhe a série Cinco coisas que você não sabia sobre Java e confira os artigos anteriores. Até a próxima!
Recursos
Aprender
- Cinco coisas que você não sabia sobre … :
Descubra o quanto você não sabe sobre a plataforma Java nesta série
dedicada a transformar informações secundárias sobre tecnologia Java em
dicas úteis de programação. - “Get a head start with JDBC 4.0 using Apache Derby”
(Victor J. Soderberg, developerWorks, agosto de 2006): Esse tutorial
demonstra algumas das funções da especificação JDBC 4.0 juntamente com o
banco de dados Apache Derby. - “JDBC 4.0 enhancements in Java SE 6”
(Srini Penchikala, OnJava.com, agosto de 2006): Mais sobre as adições
de economia de tempo aos release da versão de API JDBC mais recente. - zona de tecnologia Java do developerWorks: Centenas de artigos sobre cada aspecto da programação Java.
Obter produtos e tecnologias
- JDBC 4.0 API Specification: Obtenha a especificação JDBC mais recente e conheça-a muito bem.
- JDBC Rowset Implementations 1.0.1 Specification: Faça download do JDBC Rowset RI JDBC.
Discutir
- Participe da comunidade do My developerWorks. Conecte-se a outros usuários do developerWorks enquanto explora blogs, fóruns, grupos e wikis orientadas a desenvolvedores.
***
artigo publicado originalmente no developerWorks Brasil, por Ted Neward
Ted Neward é o diretor da Neward & Associates, onde ele dá
consultoria, orientação, ensina e faz apresentações sobre Java, .NET,
Serviços XML e outras plataformas. Ele reside perto de Seattle,
Washington.