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.
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);
}
}
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.
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(0, 2))))
.addOrder(Property.forName("numero").asc())
.list();
}
@Override
public Terminal getTerminalPorNumero(Long numero) {
return (Terminal) this.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(0, 2))).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 numero) throws 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.
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:
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();
}
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;
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!
É isso! Até a próxima!
Comentários
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();