Utilizando um pool de conexões com hibernate

Parece algo já muito falado na web. Mas o que vou tratar aqui nem todo mundo usa e as vezes sofre com certos tipos de erros.

O caso descrito é o seguinte:
Tenho um número X de agências bancárias do banco Y. E tenho que controlar as linhas telefônicas do banco.
Cada cidade tem N agências com N telefones cada. E cada agência pode ter posto de auto atendimento em outros locais. Cada posto de auto atendimento tem uma agência que é responsável por ele. Preciso saber quais são os terminais telefônicos de cada agência.

Trataremos da seguinte forma. O sistema será implementado quando as agências e suas linhas telefônicas já estão em funcionamento. Então teremos um banco de dados legado.

Vou criar um DAO genérico do tipo que tem muito tutorial por aí, depois vamos aperfeiçoar.

A vantagem do DAO genérico é que ele pode ser utilizado para executar operações persistentes de qualquer entidade.

primeiro, criamos a interface GenericDAO
import java.io.Serializable;
import java.util.List;
import org.hibernate.Session;

public interface GenericDAO<T> {
  
  public Session getSession();
  public T getById(Serializable id);
  public List<T> getAll();
  public T save(T entity);
  public void delete(T entity);
}
 
Agora a classe que implementa a interface que acabamos de criar
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.List;

import org.hibernate.Session;

import telefonia.dao.GenericDAO;

public abstract class HibernateGenericDao<T> implements GenericDAO<T> {

  private Class<T> persistentClass;
  private Session session;
  
  @SuppressWarnings("unchecked")
  public HibernateGenericDao(Session session) {
    this.session = session;
    this.persistentClass = (Class<T>) ((ParameterizedType)getClass().getGenericSuperclass()).
      getActualTypeArguments()[0];
  }
  
  @Override
  public Session getSession() {
    return this.session;
  }

  @SuppressWarnings("unchecked")
  @Override
  public T getById(Serializable id) {
    return (T)this.session.get(this.persistentClass, id);
  }

  @SuppressWarnings("unchecked")
  @Override
  public List<T> getAll() {
    return this.session.createCriteria(this.persistentClass).list();
  }

  @SuppressWarnings("unchecked")
  @Override
  public T save(T entity) {
    return (T)this.session.merge(entity);
  }

  @Override
  public void delete(T entity) {
    this.session.delete(entity);
  }
}

A classe terminal, que representa cada terminal ou linha telefônica.
@Entity
@Table (name="terminal")  
public class Terminal implements Serializable {
  
  private static final long serialVersionUID = 1L;
  
  private Long id;
  private Prefixo idPrefixo;
  private Operadora idOperadora;
  private Integer ddd;
  private Integer numero;
  private Date dataInstalacao;
    
  @Id
  @GeneratedValue
  @Column(name="id_terminal")
  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }
  
//getters e setters

Agora vamos criar o DAO para a entidade Terminal.
Essa  interface  é  uma  especialização  de  GenericDAO.  Definimos que o tipo da entidade é Terminal, quando herdamos GenericDAO.
public interface TerminalDao extends GenericDAO<Terminal> {
  public List<Terminal> getListaTerminaisPorNumeroDeTelefone(Long telefone);
  public Terminal getTerminalPorNumero(Long numero);
}

Vamos criar uma  classe  chamada  HibernateTerminalDAO  que  herda  as  implementações
do  DAO  genérico  para  Hibernate  (class  HibernateGenericDAO)  e  implementa  a  interface TerminalDAO, que é uma interface que define métodos genéricos e específicos.
public class HibernateTerminalDao extends HibernateGenericDao<Terminal> implements TerminalDao {
  public HibernateTerminalDao(Session session) {
    super(session);
  }
  @Override
  public List<Terminal> getListaTerminaisPorNumeroDeTelefone(Long telefone) {
    return this.getSession().createCriteria(Terminal.class)
    .add(Restrictions.eq("numero", telefone.toString().substring(2, telefone.toString().length())))
    .add(Restrictions.eq("ddd", Integer.parseInt(telefone.toString().substring(02))))
    .addOrder(Property.forName("numero").asc())
    .list();
    
  }
  @Override
  public Terminal getTerminalPorNumero(Long numero) {
    return (Terminalthis.getSession().createQuery("select t from Terminal t where t.numero = :tnum and t.ddd = :tddd").
        setParameter("tnum", numero.toString().substring(2, numero.toString().length())).
        setParameter("tddd", Integer.parseInt(numero.toString().substring(02))).uniqueResult();
    }
}

A classe HibernateConnection
public class HibernateConnection {
  private static final SessionFactory sessionFactory;
  static {
    try {
      sessionFactory = new AnnotationConfiguration().configure()
          .buildSessionFactory();
    catch (Throwable ex) {
      throw new ExceptionInInitializerError(ex);
    }
  }
  public static Session getSession() {
      Session session = sessionFactory.openSession();
      return session;
  }

Agora finalmente vamos criar a tal da fábrica de conexões (DAO)
public abstract class DAOFactory {
  public static DAOFactory getDAOFactory() {
    return new HibernateDAOFactory();
  }
  public abstract void startTransaction();
  public abstract void cancelTransaction();
  public abstract void finalize();
  
  public abstract TerminalDao getTerminalDao();
}

A classe HibernateDAOFactory que implementa os métodos da classe abstrata DAOFactory
public class HibernateDAOFactory extends DAOFactory 

  private Session session; 
  private Transaction tx; 
   
  public HibernateDAOFactory() { 
    this.session = HibernateConnection.getSession()
  }
  
  @Override 
  public void cancelTransaction() { 
    this.tx.rollback()
    this.tx = null
  

  @Override 
  public void startTransaction() { 
    this.tx = this.session.beginTransaction()
  
   
  @Override 
  public void finalize() { 
    if (this.tx != null) { 
      this.tx.commit()
    
    this.session.close()
  
  
  @Override
  public TerminalDao getTerminalDao() {
    return new HibernateTerminalDao(this.session);
  }
}

Agora para instanciar o DAO, vamos criar os seguintes métodos:
public Terminal getTerminalPorNumero(Long numerothrows DaoException {
    DAOFactory daoFactory = DAOFactory.getDAOFactory();
    TerminalDao terminalDao = daoFactory.getTerminalDao();
    daoFactory.startTransaction();
    Terminal t = terminalDao.getTerminalPorNumero(numero);
    daoFactory.finalize();
    return t;
  }
  
public List<Terminal> getListaTerminaisPorNumeroDeTelefone(Long telefone) {
    DAOFactory daoFactory = DAOFactory.getDAOFactory();
    TerminalDao terminalDao = daoFactory.getTerminalDao();
    daoFactory.startTransaction();
    List<Terminal> terminais = terminalDao.getListaTerminaisPorNumeroDeTelefone(telefone);
    daoFactory.finalize();
    return terminais;
  }
 
Ambos os métodos acima irão consultar na base de dados o terminal ou uma lista de terminais (caso haja repetição de um mesmo número com ddd igual para agências diferentes). Observe que nossa base de dados é legada, então esse é um erro que provocarei para explicar melhor essa fábrica de conexão.

Agora finalmente, vamos criar um método que vai verificar se existe números repetidos (mesmo ddd e número de telefone) na base de dados.

private static Terminal verificaNumeroRepetido(String ddd, String numero) {
    Long num = new Long(ddd.concat(numero));
    Terminal terminal = null;
    try {
      terminal = new TerminalService().getTerminalPorNumero(num);
    catch (NonUniqueResultException nre) {
      List<Terminal> listaTerminais = new TerminalService().getListaTerminaisPorNumeroDeTelefone(new Long(ddd.concat(numero)));
      //aqui tenho uma relação dos números repetidos. Aí o que eu decido o que faço com eles...
    
    return terminal;
  }


O método acima consulta um terminal da base pelo ddd + número. Até aí tudo bem. Caso haja repetição de ddd e número, pego a lista de números repetidos e decido qualquer coisa a ser feito com eles. 
O problema, é que quando pegar um número repetido vai gerar uma excessão do tipo NonUniqueResultException e não vai fechar a conexão, vai manter a conexão aberta.

Suponhamos que no pool de conexões do hibernate foi definido no máximo 10 conexões simultâneas:
<!-- Pool de conexoes -->
<property name="hibernate.c3p0.min_size">2</property>
<property name="hibernate.c3p0.max_size">10</property>

Isso implica que quando 10 usuários utilizarem essa rotina, ou mesmo um usuário repetir
esse processo 10 vezes, a consulta vai literalmente parar esperando uma das conexões cair para tentar instanciar uma nova conexão, ou seja, o sistema vai parar!
O correto seria fazer o sistema prevenir esse tipo de excessão e não instanciar conexões novas enquanto existirem abertas.

Melhorando a fábrica de conexões

Vamos modificar a classe HibernateConnection. É lá que instanciamos novas conexões
public class HibernateConnection {
  private static final SessionFactory sessionFactory;
  private static final ThreadLocal<Session> threadSession = new ThreadLocal<Session>();
  static {
    try {
      sessionFactory = new AnnotationConfiguration().configure()
          .buildSessionFactory();
    catch (Throwable ex) {
      throw new ExceptionInInitializerError(ex);
    }
  }
  public static Session getSession() {
    if ((threadSession.get() == null|| (!threadSession.get().isOpen())) {
      Session session = sessionFactory.openSession();
      threadSession.set(session);
      return session;
    else {
      Session session = threadSession.get();
      return session;
    }
  }
  public static void closeSession() {
    Session session = threadSession.get();
    threadSession.set(null);
    try {
      if (session != null) {
        session.close();
      }
    catch (Exception e) {
      throw new HibernateException(e);
    }
  }
}

Vejamos as alterações. Agora, quando um usuário instanciar uma nova conexão ela será incluída em uma thread que criamos do tipo Session. Quando ocorrer alguma excessão e a conexão não for fechada, o método getSession() vai verificar se a conexão ainda está na thread. Caso esteja, ela será retornada ao usuário e não vai abrir uma nova conexão com a base de dados. Isso me garante que um mesmo usuário não vai abrir N conexões gerando carga e travando minha base de dados.

Foi incluído ainda um novo método: closeSession() que é que vai ser responsável por fechar a conexão e liberá-la da thread.

A mesma coisa ocorre quando não fechamos uma transação (Transaction). Então vamos melhorar a classe HibernateDAOFactory:
public class HibernateDAOFactory extends DAOFactory {
  private Session session;
  private ThreadLocal<Transaction> threadTransaction = new ThreadLocal<Transaction>();
  
  public HibernateDAOFactory() {
    startSession();
  }
  @Override
  public void startTransaction() {
    Transaction tx = threadTransaction.get();
    try {
      if (tx == null) {
        tx = session.beginTransaction();
        threadTransaction.set(tx);
      }
    catch (Exception e) {
      throw new HibernateException(e);
    }
  }
  @Override
  public void cancelTransaction() {
    Transaction tx = threadTransaction.get();
    try {
      if ((tx != null && !tx.wasCommitted()) && (!tx.wasRolledBack())) {
        tx.rollback();
        threadTransaction.set(null);
      }
    catch (Exception e) {
      throw new HibernateException(e);
    }
  }
  @Override
  public void finalize() {
    Transaction tx = threadTransaction.get();
    try {
      if (tx != null && !tx.wasCommitted() && !tx.wasRolledBack()) {
        tx.commit();
        threadTransaction.set(null);
      }
     
    catch (Exception e) {
      cancelTransaction();
      throw new HibernateException(e);
    } finalize {
      closeSession();
    }
  }
  @Override
  public TerminalDao getTerminalDao() {
    return new HibernateTerminalDao(this.session);
  }
  
  private void startSession() {
    try {
      if (session == null || !session.isOpen()) {
        session = HibernateConnection.getSession()
      }
    catch (Exception e) {
      throw new HibernateException(e);
    }
  }
  @Override
  public void closeSession() {
    this.session = null;
    HibernateConnection.closeSession();
  }
}
 
Fizemos a mesma coisa com as transações. Incluímos ela em uma thread do tipo Transaction, e quando o usuário for instanciar uma transação, verificaremos se ela já não está aberta.

Por último, uma alteração na classe abstrata DAOFactory, vamos incluir o método closeSession() que foi implementado em HibernateDAOFactory
public abstract class DAOFactory {
  public static DAOFactory getDAOFactory() {
    return new HibernateDAOFactory();
  }
  
  public abstract Session getSession();
  public abstract void closeSession();
  
  public abstract void startTransaction();
  public abstract void cancelTransaction();
  public abstract void finalize();
  
  public abstract TerminalDao getTerminalDao();
}

Agora temos concluído nossa fábrica de conexões. O exemplo do terminal das agências foi só um pretexto pra provocar um erro possível e não ter dor de cabeça com abrir e fechar conexões. Melhore de acordo com seu código!

É isso! Até a próxima!

Comentários

Unknown disse…
Excelente post. Parabéns!
Anônimo disse…
Excelente post.

Para garantir o fechamento da conexão, vc poderia colocar um finaly no trecho abaixo, mais precisamente na parte do finalize(). O que acha?

daoFactory.startTransaction();
Terminal t = terminalDao.getTerminalPorNumero(numero);
daoFactory.finalize();
Colocar o closeSession() no finalize. Ótima dica!

Postagens mais visitadas deste blog

Gravando dados de um arquivo CSV no Oracle utlizando Python e a biblioteca cx_Oracle

Popup em JSF sem Javascript