Serialização em Java: Serializando objetos com byte com PostgreSQL
Veja neste artigo como serializar objetos em Java e salvar no banco de dados PostgreSQL.
Neste artigo veremos como trabalhar com byte em Java e com PostgreSQL. O tipo byte é muito utilizado para serialização de objetos para transferência entre pontos distintos, ou seja, sistemas distribuidos, mas não somente isto, além disso o byte é amplamente utilizado para salvar informações em considerar codifição de caracteres. Imagine, por exemplo, que você precise gravar um texto escrito em Japonês mas o encode do seu banco não suporte tal codificação, neste caso o byte é uma ótima alternativa para que você não perca o conteúdo, pois se você tentar salvar como varchar os caracteres ficarão totalmente desconfigurados e você perderá a informação.
Em Java usamos o tipo “byte” que é uma palavra reservada para definição do tipo de uma variável, mas como nosso caso sempre será vários “bytes” e não apenas 1, trabalharemos com um array de bytes, ou seja, “byte[]”.
No PostgreSQL o tipo byte é definido através da palavra reservada “bytea”, ou seja, ao gerarmos um array de bytes no Java nós salvamos no campo “bytea” no PostgreSQL. Vejamos um caso onde a aplicação do byte pode ser necessária:
“O seu sistema A recebe informações de um sistema B e deve salvar essas informações no banco de dados para depois retornar ao sistema B novamente, quando este solicitar. O sistema A não sabe qual o banco de dados e muito menos a codificação usada pelo sistema B, sendo assim como poderá o sistema A escolher um encode apropriado ? (UTF-8, ISO e etc). A melhor maneira de solucionar este problema é criar um campo no Java do tipo “byte[]” e um no PostgreSQL do tipo “bytea”, assim nós não nos preocuparemos com o tipo de informação contida.
Neste artigo vamos construir um projeto simples para demonstrar o uso do byte no Java e no PostgreSQL.
O projeto
O projeto proposto terá como objetivo salvar um objeto Cliente no banco de dados e depois retorná-lo a qualquer momento, apenas pelo seu ID.
Vejamos nosso script para criação de tabela no banco, como mostra a Listagem 1.
CREATE TABLE cliente
(
id integer NOT NULL,
objeto bytea,
CONSTRAINT cliente_pkey PRIMARY KEY (id )
)
WITH (
OIDS=FALSE
);
ALTER TABLE cliente
OWNER TO postgres;
Perceba acima que nossa tabela cliente possui dois campos: id e objeto. Onde o id é um inteiro, o que já estamos acostumados e usar e o objeto é do tipo bytea. Nosso campo objeto armazenará exatamente o que descreve, um objeto. Isso significa que poderemos armazenar um objeto cliente inteiro com todas suas dependências e depois apenas retorná-lo da forma como era, um Cliente.
Alguns podem pensar: É muito mais fácil trabalhar dessa forma, evitamos de criar centenas de campos e criamos apenas 1 campo objeto, isso é ótimo porém tem duas desvantagens:
- Performance: Pelo fato de precisar serializar e deserializar o objeto toda vez isso torna o processo mais lento;
- integridade: Você perde o controle sobre a integridade dos dados. Como você vai criar um “check” em algum campo se o campo nem existe;
- Consultas de relatórios: Torna-se inviável criar relatórios se você apenas tem informações em “byte”;
Enfim, não pense em ir colocando bytea em todas as suas tabelas, faça isso apenas quando realmente for necessário.
O nosso projeto irá conter apenas três classes, são elas:
- Conexao.java: Classe responsável por realizar a conexão com o banco de dados;
- Cliente.java: Nosso bean que será salvo no banco;
- ClienteMainSave.java: Classe principal, com método main, responsável por salvar e retornar o objeto cliente do banco de dados.
- ClienteMainGet.java: Classe principal, com método main, responsável por retornar o objeto cliente do banco de dados.
Vejamos a nossa classe Conexao.java presente na Listagem 2.
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class Conexao {
private static Connection connection;
public static Connection getConnection() {
if (connection == null) {
try {
Class.forName("org.postgresql.Driver");
String host = "localhost";
String port = "5432";
String database = "teste";
String user = "postgres";
String pass = "postgres";
String url = "jdbc:postgresql://" + host + ":" + port + "/"
+ database;
connection = DriverManager.getConnection(url, user, pass);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return connection;
}
public static void close() {
if (connection == null){
throw new RuntimeException("Nenhum conexao aberta");
}else{
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
Nossa classe Conexao não é o objetivo principal do nosso artigo mas faz-se necessária para que nosso projeto funcione corretamente. No método getConnection() definimos as configurações necessárias para conectar-se ao nosso banco de dados, lembrando que você deve importar o “.jar” do driver PostgreSQL JDBC ao seu projeto. O método close() simplesmente fecha a conexão quando necessário, mas fazendo uma checagem com antecedência para evitar chamar o método close() em um objeto nulo.
Você não precisa se preocupar sobre detalhes da conexão, pois não é o foco do nosso artigo. Apenas mude as configurações das variáveis:
- host: IP da máquina onde está o banco de dados ou localhost se for a sua própria máquina;
- port: Porta usada para o PostgreSQL, geralmente 5432 se não foi alterada;
- database: Nome do banco de dados;
- user: Usuário do banco de dados;
- pass: Senha do banco de dados;
Definindo corretamente as variáveis acima e importando o driver do PostgreSQL no seu projeto a conexão será com sucesso.
Perceba também que nossos método são todos estáticos, pois não há necessidade de instanciar a classe conexão, precisaremos apenas capturar a conexão e usá-la.
import java.io.Serializable;
import java.util.Date;
public class Cliente implements Serializable {
private int id;
private String nome;
//m = masculino, f = feminino
private char genero;
private Date dataNascimento;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
public char getGenero() {
return genero;
}
public void setGenero(char genero) {
this.genero = genero;
}
public Date getDataNascimento() {
return dataNascimento;
}
public void setDataNascimento(Date dataNascimento) {
this.dataNascimento = dataNascimento;
}
}
Nosso Bean Cliente, presente na Listagem 3, implementa a interface Serializable, e isto é necessário para conseguirmos salvar o objeto Cliente no banco como um byte[]. Objetos sem a interface Serializable não podem sofrer serialização e muito menos deserialização, sendo assim ficamos impedidos de salvar o Cliente como byte no banco de dados.
Na serialização do objeto apenas o nome dos atributos e valores são conservados, os métodos getters e setters não são, você verá isso mais a frente.
Vejamos os dois métodos que farão a “mágica” necessária para salvar o cliente como byte[], como mostra a Listagem 4.
private static byte[] converterClienteParaByte(Cliente cliente) {
try {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream ous;
ous = new ObjectOutputStream(bao);
ous.writeObject(cliente);
return bao.toByteArray();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
O ObjectOutputStream recebe como argumento no seu construtor um ByteArrayOutputStream que servirá para retornar nosso array de byte, o ObjectOutputStream escreve o nosso objeto serializando o mesmo, por isso ele tem que implementar Serializable. O retorno do nosso método é um byte[]. Agora vejamos o processo inverso, transformar no array de byte (byte[]) em um objeto do tipo Cliente.
private static Cliente converterByteParaCliente(byte[] clienteAsByte) {
try {
ByteArrayInputStream bao = new ByteArrayInputStream(clienteAsByte);
ObjectInputStream ous;
ous = new ObjectInputStream(bao);
return (Cliente) ous.readObject();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
Note que na Listagem 4 o nosso ByteArrayInputStream não recebe nada no construtor, porém, na Listagem 5 recebe um array de byte. Esse objeto é passado ao ObjectInputStream que com o readObject() faz a conversão do byte[] para o Object, que em nosso caso esta sendo feito um Cast para Cliente.
Você já deve ter percebido que o primeiro método será usado para salvar o objeto no banco e o segundo método será usado para retornar o objeto do banco. Estes tem muito mais utilidades que apenas salvar e recuperar dados do banco, principalmente trantando-se de transferência de dados em rede. Para quem está trabalhando com Socket é uma ótima pedida para envio de arquivos.
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;
public class ClienteMainSave {
/**
* @param args
*/
public static void main(String[] args) {
Cliente cliente = new Cliente();
cliente.setId(1);
cliente.setDataNascimento(new Date());
cliente.setGenero(''m'');
cliente.setNome("DevMedia");
salvarCliente(cliente);
}
private static void salvarCliente(Cliente cliente) {
try {
Connection con = Conexao.getConnection();
PreparedStatement ps = con.prepareStatement("INSERT
INTO cliente(id, objeto) values (?, ?)");
ps.setInt(1, cliente.getId());
ps.setBytes(2, converterClienteParaByte(cliente));
ps.execute();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private static byte[] converterClienteParaByte(Cliente cliente) {
try {
ByteArrayOutputStream bao = new ByteArrayOutputStream();
ObjectOutputStream ous;
ous = new ObjectOutputStream(bao);
ous.writeObject(cliente);
return bao.toByteArray();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
Vamos entender passo a passo a nossa Listagem 6:
- No nosso método main() criamos o objeto Cliente e definimos todos os seus campos, após essas definições nós passamos o objeto Cliente ao método salvarCliente();
- O método salvarCliente() monta um PreparedStatement para inserir o registro no banco, mas precisamos converter o Cliente em um array de bytes e para isso chamamos o método converterClienteParaByte() que já foi explicado anteriormente;
Até este ponto já temos o registro salvo no banco de dados contendo o ID seguido de uma sequência de bytes. Agora vejamos como capturar essas informações, como mostra a Listagem 7.
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class ClienteMainGet {
/**
* @param args
*/
public static void main(String[] args) {
Cliente cliente = getClienteFromDB(1);
System.out.println(cliente.getId());
System.out.println(cliente.getGenero());
System.out.println(cliente.getNome());
System.out.println(cliente.getDataNascimento());
}
private static Cliente getClienteFromDB(int id) {
try {
Connection con = Conexao.getConnection();
PreparedStatement ps = con.prepareStatement("SELECT objeto
FROM cliente WHERE id = ?");
ps.setInt(1, id);
ResultSet rs = ps.executeQuery();
rs.next();
return converterByteParaCliente(rs.getBytes(1));
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
private static Cliente converterByteParaCliente(byte[] clienteAsByte) {
try {
ByteArrayInputStream bao = new ByteArrayInputStream(clienteAsByte);
ObjectInputStream ous;
ous = new ObjectInputStream(bao);
return (Cliente) ous.readObject();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
}
A lógica nessa classe é o inverso da mostrada na Listagem 6, pois aqui nós chamamos o método getClienteFromDB() passando o id do cliente. Internamente este método percorre o ResultSet retornando o campo “objeto” do banco de dados, porém como sabemos este campo é um array de bytes e precisamos converte-lo para um objeto Cliente, sendo assim chamamos o método converterByteParaCliente() que também já foi explicado anteriormente.
Veja que ao realizar um select no nosso banco de dados retornando o conteúdo do campo “objeto” temos o seguinte retorno. Observe a Listagem 8.
DML:
SELECT objeto FROM cliente
Saída:
"\254\355\000\005sr\000\007Cliente\364\221#b\262p7\374\002
000\004C\000\006generoI\000\002idL\000
016dataNascimentot\000\020Ljava/util/Date;L\000\004nomet\000
022Ljava/lang/String;xp\000m\000\000\000
001sr\000\016java.util.Datehj\201\001KYt\031\003\000\000xpw\010
000\000\001J\274ca\261xt\000\010DevMedia"
Todo conteúdo acima é deserializado para construir o objeto que nós serializamos. Lembra que falamos logo no início que os métodos não são serializados, apenas seus atributos e valores. Isso pode ser percebido na listagem 8.
Um pouco sobre bytea no PostgresQL
No PostgreSQL, o bytea armazena um “binary String” que na verdade é um array de bytes. Cada byte contém 8 bits e um sequência de bytes podem conter inúmeros bits.
A questão é que o PostgreSQL possui dois tipos de tratamento para o bytea: hex e escape. Quando usado o formato hex o tipo bytea usa 2 hexadecimais por cada byte armazenado, além de ser aceito tanto no input como no output das informações, ou seja, tanto da escrita das informações como na leitura destas. O formato hex é mais novo e só está presente nas versões 9.0 e posteriores do PostgreSQL. Por outro lado o formato “escape” é mais tradicional e antigo, utilizando a representação de caracteres em “ASCII”.
Neste artigo estudamos como utilizar o tipo byte para trabalhar com dados serializados, passando o objeto cliente de um lado ao outro sem se preocupar com os campos que o mesmo possui. Como citamos anteriormente, esse processo é muito útil quando trabalhamos com transferência de dados na Rede, pois além de garantir a integridade da informação, conseguimos minimizar a complexidade na recupereção dessa informação.
O PostgreSQL já nos da um tipo especial de campo para trabalhar com byte, o bytea que armazena um array de bytes podendo estes serem formatados usando “hex” ou “escape”. Explicamos um pouco sobre cada um desses tipos na última seção, mas vale ressaltar que o “hex” é o tipo mais novo e só está presente na versão 9.0 ou posterior do PostgreSQL, além de ser a mais indicada para aplicação que usem tal versão do banco.
Artigos relacionados
-
Artigo
-
Artigo
-
Artigo
-
Artigo
-
Vídeo