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;
Listagem 1. DDL para criação da tabela cliente

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:

  1. Performance: Pelo fato de precisar serializar e deserializar o objeto toda vez isso torna o processo mais lento;
  2. integridade: Você perde o controle sobre a integridade dos dados. Como você vai criar um “check” em algum campo se o campo nem existe;
  3. 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:

  1. Conexao.java: Classe responsável por realizar a conexão com o banco de dados;
  2. Cliente.java: Nosso bean que será salvo no banco;
  3. ClienteMainSave.java: Classe principal, com método main, responsável por salvar e retornar o objeto cliente do banco de dados.
  4. 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(); } } } }
Listagem 2. Conexao.java/div>

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:

  1. host: IP da máquina onde está o banco de dados ou localhost se for a sua própria máquina;
  2. port: Porta usada para o PostgreSQL, geralmente 5432 se não foi alterada;
  3. database: Nome do banco de dados;
  4. user: Usuário do banco de dados;
  5. 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; } }
Listagem 3. Cliente.java

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; }
Listagem 4. Converter Cliente para byte[]

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; }
Listagem 5. Converter byte[] para Cliente

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; } }
Listagem 6. Classe ClienteMainSave.java

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; } }
Listagem 7. Classe ClienteMainGet.java

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"
Listagem 8. Conteúdo do campo objeto

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