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.