Um dos recursos mais chamativos dos bancos de dados modernos é o suporte a informações não-estruturadas, que fogem aos tipos de dados simples como inteiros, datas e strings. Bancos de dados com capacidade de armazenar imagens, planilhas e outros tipos de documentos, lado a lado com informações estruturadas em campos e registros, ganharam até nomes pomposos: “bancos de dados multimídia” e “universal databases”. Estes bancos têm inúmeras aplicações em comércio eletrônico, sistemas de segurança, recursos humanos, entre outras possibilidades.
Guia do artigo:
- Inicializando o BD
- BLOBs, CLOBs e Streams
- O visualizador de imagens
- Lendo um BLOB do banco, via streams
- Threads no Swing
- Conclusões
Mas lidar com dados não-estruturados na maioria dos bancos relacionais não é tarefa trivial. A programação em geral foge ao estilo usual do SQL, e obter performance adequada exige conhecimento avançado tanto do desenvolvedor de aplicações quando do administrador do banco de dados. Na essência, dados multimídia são armazenados em bancos de dados como grandes “campos binários”, os BLOBs – Binary Large Objects. Eles não estão restritos pelas limitações de tamanho de campos decimal e varchar tradicionais; por outro lado estes campos não podem ser utilizados livremente para compor índices ou cláusulas where.
Saiba mais: Guia Completo de SQL
Felizmente a linguagem Java torna as coisas mais fáceis, ao oferecer como parte do padrão JDBC alguns métodos e classes especialmente criados para a manipulação dos BLOBs.
Este artigo apresenta uma aplicação de visualização de imagens, que permite ler imagens em qualquer formato suportado pelo Java (como GIF, PNG e JPEG), oriundas tanto de arquivos em disco quando de um banco de dados. A aplicação de exemplo também demonstra melhores práticas no desenvolvimento Swing, ao garantir que a interface com o usuário não fique “congelada” durante a carga das imagens.
Os exemplos foram testados com três bancos de dados: HSQLDB, MySQL e PostgreSQL. Já as fotos que ilustram os exemplos foram obtidas no site do jornal italiano La Republica, em matéria onde a agência de notícias Reuters elege as melhores fotos do ano no jornalismo europeu: www.repubblica.it/2006/05/gallerie/esteri/reuters-foto-anno/2.html.
Para acompanhar este artigo, é importante que o leitor tenha conhecimentos básicos de Swing, APIs de E/S do Java (pacote java.io) e configuração do banco de dados utilizado. A série de artigos sobre os fundamentos do JDBC, publicados nas edições 42 e 43, servem como ponto de partida.
Inicializando o BD
A forma mais simples de se lidar com campos BLOB é simplesmente tratá-los da mesma forma que seriam tratados outros tipos de dados pelo JDBC: eles podem ser manipulados como se fossem simples valores passados como parâmetros para comandos SQL e recuperados como parte de resultados de consultas.
Mais especificamente, as interfaces ResultSet e PreparedStatement (em java.sql) fornecem os métodos getObject() e setObject(), que permitem ler e salvar objetos Java quaisquer, fazendo automaticamente as conversões necessárias.
Entretanto, muitos drivers JDBC irão serializar e desserializar automaticamente objetos Java passados para estes métodos, o que pode causar problemas de portabilidade dos dados para aplicações não-Java. Para evitar problemas, coloque a informação desejada em seu formato “nativo”, dentro de um array de bytes (byte[]). Assim é evitado o uso da serialização com o banco de dados. Será responsabilidade da aplicação converter este array de bytes no objeto Java desejado.
A Listagem 1 apresenta o código de um programa simples (InsereImagem.java) que recebe o nome de um arquivo e uma descrição deste arquivo pela linha de comando. O arquivo e a sua descrição são então inseridos no banco de dados da aplicação de exemplo. A mesma listagem mostra exemplos da execução deste programa, utilizando as imagens inclusas junto com os fontes para download deste artigo.
import java.sql.*;
import java.io.*;
import java.util.*;
class InsereImagem
{
public static void main(String args[]) throws Exception {
// valida a linha de comando
if (args.length != 2) {
System.err.println("Uso: InsereImagem ");
System.exit(-1);
}
// lê a imagem para um byte[]
File img = new File(args[0]);
byte[] imagem = new byte[(int)img.length()];
System.out.println("Lendo " + img.length() + " bytes...");
DataInputStream is = new DataInputStream(
new FileInputStream(args[0]));
is.readFully(imagem);
is.close();
// obtém parâmetros de conexão via arquivo de propriedades
Properties parametrosConexao = new Properties();
parametrosConexao.load(InsereImagem.class.getResourceAsStream(
"banco.properties"));
String driver = parametrosConexao.getProperty("driver");
String url = parametrosConexao.getProperty("url");
String login = parametrosConexao.getProperty("login");
String senha = parametrosConexao.getProperty("senha");
// conecta ao banco e insere a imagem
Class.forName(driver);
Connection con = DriverManager.getConnection(
url, login, senha);
PreparedStatement stmt = con.prepareStatement(
"insert into imagens (nome, descricao, tamanho, imagem) " +
"values (?, ?, ?, ?)");
stmt.setString(1, img.getName());
stmt.setString(2, args[1]);
stmt.setInt(3, (int)img.length());
stmt.setObject(4, imagem);
stmt.executeUpdate();
// finaliza o programa
stmt.close();
con.close();
System.out.println("Imagem " + img.getName() + " acrescentada ao banco");
}
}
Exemplo da execução do programa InsereImagem
$ java InsereImagem fotos/reuters93679830812142339_big.jpg "vulcão em erupção"
Lendo 20868 bytes...
Imagem reuters93679830812142339_big.jpg acrescentada ao banco
$ java InsereImagem fotos/reuters93679810812142350_big.jpg "sapo ao resgate"
Lendo 34887 bytes...
Imagem reuters93679810812142350_big.jpg acrescentada ao banco
$ java InsereImagem fotos/reuters93679880812142335_big.jpg "nado sincronizado"
Lendo 46264 bytes...
Imagem reuters93679880812142335_big.jpg acrescentada ao banco
$ java InsereImagem fotos/reuters93679650812142416_big.jpg "turbantes"
Lendo 22677 bytes...
Imagem reuters93679650812142416_big.jpg acrescentada ao banco
Este programa, assim como a aplicação de exemplo, utiliza o arquivo jdbc.properties para fornecer os parâmetros de conexão ao banco de dados. O conteúdo deste arquivo, assim como os scripts para a inicialização do banco de exemplo com os três bancos testados, são listados e comentados no quadro “BLOBs em bancos livres”.
Foi utilizado um algoritmo bastante simples para a leitura das imagens, baseado no método readAll() de DataInputStream, que dificilmente seria adequado para aplicações reais. Mas serve bem para nossos propósitos imediatos. Mais adiante veremos como aplicações profissionais devem lidar com arquivos e BLOBs, utilizando outros métodos de DataInputStream.
Uma vez inicializado o banco de dados com algumas imagens e suas descrições, tome cuidado com as consultas que serão realizadas sobre ele. Alguns bancos de dados (por exemplo o MySQL) irão tentar exibir a informação em um BLOB literalmente, sem nenhuma conversão ou formatação, se consultados pelo seu prompt para execução de comandos SQL interativos. Outros bancos (como o PostgreSQL e o HSQLDB) irão tentar gerar alguma representação do campo na forma de uma string, que poderia ser usada por um utilitário de importação e exportação dos dados. Infelizmente, na maioria dos casos não será possível visualizar as imagens armazenadas diretamente pelos programas fornecidos com o seu banco de dados.
No MySQL é possível direcionar o conteúdo de um BLOB para um arquivo em disco (no servidor), por exemplo:
select imagem into dumpfile "/tmp/t.jpg" from imagens where id=1;
Então seria possível abrir o arquivo /tmp/t.jpg com um navegador web ou outro visualizador de imagens, para verificar se a imagem foi gravada corretamente no banco de dados.
BLOBs, CLOBs e Streams
Até agora vimos que é bem simples lidar com campos BLOB em aplicações Java, quando se tem certeza de que todos os dados a serem armazenados nestes campos serão pequenos (até algumas dezenas de Kb). Basta utilizar um byte[] e chamar PreparedStatement.setObject() e ResultSet.getObject().
Caso as informações sejam sempre textuais, como páginas HTML e documentos XML, é possível utilizar campos CLOB (Character Large Object). A vantagem de um CLOB sobre um BLOB é que a maioria dos bancos serão capazes de fazer operações como busca textual, conversão de codificações de caracteres (como UTF-8 e Latin-1 [1]) e conversão de finais de linha entre os formatos do Windows, Unix e MacOS sobre um CLOB. Bancos mais recentes serão capazes de manipular diretamente documentos XML armazenados em CLOBs, por exemplo realizando transformações XSLT ou executando expressões XPath. Já um BLOB dificilmente permite a realização de qualquer operação além do seu armazenamento e recuperação.
Não apresentaremos exemplos de campos CLOB para não alongar demais o artigo, mas todas as técnicas utilizadas aqui com BLOBs valem para CLOBs. É apenas questão de trocar o byte[] por um char[].
Quando as informações a serem armazenadas em um BLOB ocuparem vários megabytes, a situação se torna mais complicada. Além das dificuldades para administração do banco de dados em si (como o crescimento do log de transações e das áreas para armazenar resultados intermediários da execução de consultas), temos o problema de que a leitura e o armazenamento destes campos pode demorar um tempo substancial. Durante este tempo, a aplicação irá parecer estar “congelada”.
Para que seja possível a uma aplicação ler ou salvar o conteúdo de um BLOB por partes, e assim exibir alguma informação de progresso da operação para o usuário, o conteúdo de um BLOB pode ser acessado como um stream do pacote java.io, de modo similar ao feito para a manipulação de arquivos. Como benefício adicional, o uso de streams com BLOBs permite programar operações que atuam apenas sobre uma parte do conteúdo do BLOB, ou que não necessitam ter todo o BLOB carregado em memória.
Antes de entrar nos detalhes sobre esta forma superior de se utilizar BLOBs em Java, vamos apresentar a aplicação de exemplo deste artigo.
O visualizador de imagens
O visualizador de imagens é formado por duas janelas, apresentadas na Figura 2.
A primeira é um JFrame (bancoimagens.visao.Visualizador) que exibe a imagem carregada, utilizando para tal um JLabel, e fornece botões para “importar” uma imagem (ler de um arquivo) ou para buscá-la no banco de dados [2]. Esta janela também exibe uma barra de progresso durante a leitura de uma imagem, e um botão junto à barra de progresso permite cancelar a carga da imagem.
A segunda janela é um JDialog (bancoimagens.visao.BuscaBanco) que permite indicar uma palavra-chave para a busca de imagens armazenadas no banco de dados. A palavra será procurada tanto no nome quanto na descrição da imagem. Se não for digitada nenhuma palavra, serão listadas todas as imagens armazenadas ordenadas pelo seu nome. Os resultados da busca são exibidos em um JList simples, e um deles pode ser selecionado e aberto no JFrame.
A aplicação foi construída segundo o modelo MVC (Modelo-Visão-Controlador) e é formada pelas classes apresentadas na Figura 3, como um projeto do NetBeans, e também na Figura 4, como um diagrama de classes UML.
As classes em amarelo são da própria aplicação, enquanto que as em azul são classes do Java SE que desempenham um papel crítico na aplicação
A classe bancoimagens.modelo.ImagemDAO é quem efetivamente recupera as imagens do banco de dados. Já na leitura de imagens em arquivo violamos a arquitetura MVC, inserindo o código diretamente dentro de bancoimagens.controle.Controlador. Na verdade, não é trivial isolar as camadas de modelo e controle do MVC quando se espera fornecer um feedback sobre o progresso de uma aplicação demorada. É para resolver dificuldades arquiteturais deste tipo que foram criados os frameworks de aplicações desktop como o NetBeans Platform e o Eclipse RCP.
A única classe do pacote bancoimagens.controle é responsável por responder aos eventos dos botões de ambas as janelas, coordenando as operações de leitura e importação de imagens, e a busca por palavras-chave no banco de dados.
As classes restantes têm papéis secundários na aplicação. A classe bancoimagens.modelo.DadosImagem é apenas um VO (Value Object, às vezes chamado de DTO – Data Transfer Object), que reúne os dados de uma imagem armazenada no banco: seu id, nome, descrição e tamanho. A imagem em si não é colocada dentro do VO, e deve ser lida em separado. bancoimagens.visao.ActionSupport é apenas um utilitário para ajudar na propagação de eventos ActionEvent (do Swing/AWT) das janelas até o controlador. Já bancoimagens.visao.FiltroImagens é um filtro para um JFileChooser do Swing, que permite filtrar a listagem de arquivos a apenas aqueles com extensões conhecidas de arquivos de imagem.
Lendo um BLOB do banco, via streams
O visualizador de imagens nos permite uma maneira mais conveniente de verificar o funcionamento do primeiro exemplo, InsereImagem.java. É possível localizar e visualizar qualquer uma das imagens já armazenadas no banco de dados, e visualizar também outras imagens em disco, fora do banco.
Melhor ainda, o visualizador exibe uma barra de progresso na parte inferior da janela enquanto a imagem é carregada, como pode ser visto na Figura 5. O botão “X” vermelho permite que a carga da imagem seja cancelada.
A Listagem 2 mostra o código do método ImagemDAO.abreImagem(), que retorna um campo BLOB como um InputStream. Este stream é depois encapsulado pelo controlador em um DataInputStream, para a leitura das informações binárias em formato nativo.
ImagemDAO.abreImagem()
public InputStream abreImagem(int id) throws SQLException {
String sql = "select imagem from imagens "
+ "where id = ?";
InputStream is = null;
try {
stm = con.prepareStatement(sql);
stm.setInt(1, id);
rs = stm.executeQuery();
while (rs.next()) {
is = rs.getBinaryStream(1);
}
return is;
}
finally {
limpa();
}
}
Controlador.abrir()
private void abrir() {
visualizador.lendoImagem(true);
DadosImagem dadosImagem = dlg.getDadosImagemSelecionada();
DataInputStream is;
try {
is = new DataInputStream(
dao.abreImagem(dadosImagem.getId()));
leitor = new LeitorImagens(is, visualizador, dadosImagem.getTamanho());
leitor.start();
}
// ... bloco catch omitido para poupar espaço
}
LeitorImagem.run()
public void run() {
try {
visualizador.atualizaProgresso(0, "Lendo imagem...");
if (leImagem(is, tamanho)) {
Icon icone = new ImageIcon(bytes);
visualizador.atualizaProgresso(100, null);
visualizador.exibeImagem(icone);
}
else {
visualizador.atualizaProgresso(-1, null);
}
}
// ... blocos catch e finally omitidos para poupar espaço
}
LeitorImagem.leImagem()
private boolean leImagem(DataInputStream is, int tamanho)
throws IOException, InterruptedException {
bytes = new byte[tamanho];
// uma aplicação real usaria blocos maiores
// (até 8k ou 16k para o disco, ou perto de 4K para ethernet)
// e não teria a chamada a sleep
int bloco = 1024;
int dormir = 20;
int posicao = 0;
while (!cancelado && posicao < tamanho
&& is.read(bytes, posicao, bloco) > -1) {
visualizador.atualizaProgresso((posicao * 100) / tamanho, null);
posicao += bloco;
if (posicao + bloco > tamanho) {
bloco = tamanho - posicao;
}
sleep(dormir);
}
return !cancelado;
}
Observe a simplicidade do método. O campo BLOB é consultado por meio de um comando SQL select, da maneira usual, e o resultado deste comando é vinculado a um ResultSet do JDBC. Então é utilizado o método getBinaryStream() do ResultSet para obter um InputStream que fornece acesso ao conteúdo do campo BLOB. Este stream é retornado pelo método.
O método ImagemDAO.abrirImagem() é chamado por Controlador.abrir(), também apresentado na Listagem 2. Veja que este método faz pouco mais do que chamar o DAO e passar o stream retornado para a classe interna LeitorImagens. Esta classe é um thread que faz a leitura da imagem na retaguarda, de modo que o thread de eventos do Swing pode responder ao botão de cancelar a carga da imagem, e também a outros eventos como redimensionamento e movimentação da janela principal da aplicação durante a carga.
O método run() do thread de carga (leitura) da imagem cuida de tarefas como habilitar e desabilitar os botões da janela principal (Visualizador) e faz o grosso do trabalho no método LeitorImagem.leImagem(), também exibido na Listagem 2. Este método é bem simples, lendo o InputStream em blocos de 1 Kb (conforme a variável bloco), e acumulando tudo no array bytes.
Foi necessário fazer uma pequena “trapaça” neste método para que o medidor de progresso fique visível com as fotos fornecidas com o download, e para que o leitor possa testar o cancelamento da carga de uma imagem. Trata-se de uma chamada a sleep() (herdado de java.lang.Thread). Sem esta pausa, a carga da maioria das imagens seria tão rápida que não seria possível validar o correto funcionamento da aplicação.
Em uma aplicação real, envolvendo imagens de alta resolução (portanto bem maiores) e um banco de dados remoto sendo acessado concorrentemente por vários usuários, a leitura das imagens não seria tão rápida, e assim a chamada a sleep() “simula” a performance esperada de uma aplicação real. É claro que, em uma aplicação real, você não irá inserir chamadas a sleep() desnecessárias como esta.
Finda a chamada a leImagem(), o método run() usa os bytes lidos para criar um ImageIcon (do Swing), que automaticamente identifica o formato da imagem, e avança o medidor de progresso para 100%. O ImageIcon é então passado para o Visualizador e exibido para o usuário.
É interessante observar que o método importar(), que faz a abertura de uma imagem armazenada em um arquivo em disco, faz uso do mesmo thread LeitorImagens. Conseqüentemente, este método usa a mesma funcionalidade para dar feedback sobre o progresso da operação e para permitir o seu cancelamento.
Uma vez que foi obtido o stream para leitura (ou gravação) de um BLOB, este stream pode ser utilizado como se fosse um arquivo comum, por métodos que não têm nenhum conhecimento de que este é um objeto oriundo do banco de dados. É possível, por exemplo, utilizar as facilidades do Java para criptografar e compactar arquivos, ou então usar os recursos do Lucene (lucene.apache.org) para indexação de texto.
Threads no Swing
O código para a carga de uma imagem do banco para a janela principal da aplicação provavelmente foi um pouco mais complexo do que o esperado pelo leitor. Isso porque foi usado um thread independente para realizar a leitura da imagem. Infelizmente este thread é necessário, pois caso a leitura da imagem seja feito dentro de um método de tratamento de eventos do Swing, não será possível observar o avanço do medidor de progresso, e o usuário terá a impressão de que a aplicação ficou congelada até o término da carga da imagem.
Tampouco seria possível cancelar a carga da imagem, se esta não fosse realizada por um thread em separado. O motivo é que o Swing processa os eventos um de cada vez, em fila. Todo o código envolvido no tratamento de um evento (como o ActionEvent resultante do clique sobre o botão Abrir do diálogo de busca) deve ser completado antes que o Swing passe para o próximo evento da fila. E a atualização de um controle na tela, como o JProgressBar, também é um evento.
Mas criar um thread em separado para a leitura da imagem ainda não é suficiente. Nenhum thread diferente do thread de tratamento de eventos do Swing pode modificar propriedades de um componente Swing. Caso algum thread tente, o resultado é indefinido, e pode até derrubar a JVM. Então o thread de carga de imagens não pode modificar diretamente o valor do JProgressBar, para indicar que mais um bloco de dados foi lido e estamos mais perto de completar a operação.
O Swing resolve este problema fornecendo a classe EventQueue, com o método invokeLater(). Este método recebe um java.lang.Runnable como argumento e coloca este objeto na fila de eventos, para execução posterior.
Como a chamada freqüente a invokeLater() é um tanto tediosa, foram definidos métodos na classe Visualizador que chamam este método passando objetos Runnable pré-definidos também em Visualizador, como classes internas.
Por exemplo, o thread de carga da imagem chama o método exibirImagem() quando a carga estiver completa, para que a imagem seja vinculada a um JLabel e assim exibida para o usuário. A Listagem3 apresenta a definição deste método e do Runnable utilizado por ele.
public void exibeImagem(Icon imagem) {
EventQueue.invokeLater(new ExibeImagem(
this, imagem));
}
private class ExibeImagem implements Runnable
{
private Visualizador visualizador;
private Icon imagem;
public ExibeImagem(Visualizador visualizador,
Icon imagem) {
this.visualizador = visualizador;
this.imagem = imagem;
}
public void run() {
imagem.setIcon(imagem);
}
}
O método exibeImagem() apenas chama invokeLater(), passando como argumento uma instância da classe interna ExibeImagem. Esta classe se limita a salvar em variáveis de instância os argumentos passados ao construtor, que são depois utilizados pelo seu próprio método run().
A Figura 6 esclarece melhor o fluxo de controle entre as classes dentro de cada thread. O evento ActionEvent é recebido por Visualizador, que o repassa para Controlador. Controlador então inicia o thread da aplicação, que usa o ImagemDAO para ler a imagem e ao fim repassa o controle para a classe interna Visualizador.ExibeImagem. Esta classe modifica o JLabel imagem – mas dentro do thread do Swing, e não dentro do thread da aplicação.
Os métodos ocupado() e atualizaProgresso() da classe Visualizador seguem o mesmo modelo, de encapsular chamadas a métodos do Swing dentro de um Runnable que é executado via invokeLater(). Na verdade, sempre que um thread de aplicação necessitar chamar métodos do Swing, ele deve fazê-lo por este mecanismo, e não há lugar melhor do que as classes de visão para encapsular este comportamento.
Conclusões
Este artigo apresentou como lidar com BLOBs utilizando os recursos padrão da API JDBC do Java, de modo independente do banco de dados. Vimos que o processo é simples e não há necessidade de poluir o código com classes específicas (proprietárias) para um driver JDBC. Melhor ainda, os recursos do JDBC permitem o aproveitamento de código escrito originalmente para lidar com arquivos em disco, pois tanto a manipulação de BLOBs quando de arquivos no Java são baseados no conceito de streams do java.io.
O lado do JDBC é simples, mas para se ter uma aplicação com aparência e comportamento profissional, é importante prestar atenção ao correto gerenciamento de threads em aplicações Swing. O código resultante não é trivial para desenvolvedores não habituados a programar com threads. Felizmente o modelo apresentado neste artigo pode ser reutilizado para a maioria das situações em que ocorrem tarefas demoradas, mesmo aquelas que não envolvem bancos de dados nem imagens.
Vimos também que se deve executar operações demoradas da aplicação em threads separados, e estes threads devem usar o método invokeLater() do Swing, sempre que for necessário modificar propriedades de componentes visuais. Assim, obedecendo a poucas regras, e tendo o cuidado de seguir uma arquitetura com papéis bem-definidos (como o MVC) se torna fácil lidar de modo eficiente com threads em aplicações Swing.
BLOBs em bancos livres
No HSQLDB
O script SQL para criação do banco de dados no HSQLDB é bem direto:
create table imagens (
id identity primary key,
nome varchar(50),
descricao varchar(200),
tamanho integer,
imagem longvarbinary)
O HSQLDB usa os tipos ANSI SQL long varbinary e long varchar para campos BLOB e CLOB, respectivamente, mas sem inserir um espaço depois da palavra “long” (de modo a manter seu parser SQL bem simples). Em muitos bancos de dados, os tipos long var* devem ser seguidos por uma especificação de tamanho, de modo semelhante ao varchar, mas o HSQLDB não reconhece esta especificação.
No MySQL
Já para o MySQL este seria o script:
create table imagens (
id integer auto_increment primary key,
nome varchar(50),
descricao varchar(200),
tamanho integer,
imagem blob);
Neste caso, o nome do tipo é o popular blob (e clob para campos de texto) que surgiu antes da padronização ANSI. O MySQL emprega todas as otimizações que se pode esperar para campos BLOB, por exemplo armazenar o seu conteúdo em uma área separada dos campos não-BLOB da mesma tabela, de modo que operações de busca e indexação sobre estes campos não sejam penalizadas.
No PostgresSQL
Por fim, para o PostgreSQL o script seria:
create sequence seq_imagens;
create table imagens (
id integer default nextval("seq_imagens") primary key,
nome varchar(50),
descricao varchar(200),
tamanho integer,
imagem bytea);
Como o PostgreSQL não fornece um tipo de auto-incremento, este recurso foi simulado com o uso de seqüências. O mesmo artifício seria utilizado no Oracle.
Aqui temos um tipo totalmente diferente dos demais, chamado de bytea (byte array). Este nome e seu funcionamento fazem sentido depois que se compreende a natureza Objeto-Relacional do PostgreSQL (mas explicar esta natureza estaria fora do escopo deste artigo). Para nossos objetivos, basta saber que um bytea é um BLOB para todos os efeitos equivalente aos tipos blob e long varbinary de outros bancos, e que o PostgreSQL é bastante eficiente no seu manuseio.
- Jornal italiano La Republica, onde foram obtidas as fotos de exemplo deste artigo
- Para baixar a qualquer versão da especificação JDBC
- Capítulo do Java Tutorial sobre ícones e imagens
- Capítulo do Java Tutorial sobre threads no Swing
- Site oficial do HSQLDB
- Site oficial do MySQL
- Site oficial do PostgreSQL
- UTF-8 é o padrão do Linux para acentos e permite representar caracteres de qualquer idioma conhecido, utilizando vários bytes para cada caractere. Já o Latin-1 é o padrão do Windows para acentos, e por utilizar apenas um byte por caractere, consegue representar inteiramente apenas alfabetos da Europa Ocidental (incluindo, é claro, o português).
- Deixamos como exercício para o leitor programar as operações de inserir uma nova imagem no banco de dados, e de deletar uma imagem, além da edição da sua descrição. A inserção consiste em realizar a gravação do BLOB usando setStream() de PreparedStatement; a deleção consiste em simplesmente apagar no banco de dados a linha que contém o BLOB. Ambos devem ser fáceis de implementar tomando como base o código apresentado neste artigo.