UNIVERSIDADE FEDERAL DE SANTA CATARINA – UFSC CENTRO TECNOLÓGICO – CTC DEPARTAMENTO DE INFORMÁTICA E ESTATÍSTICA – INE CURSO DE SISTEMAS DE INFORMAÇAO ARTIGO – Framework Hibernate Daniel Costa Smolenaars Fabíola Pavan Costa João Bosco Mangueira Sobral Orientador Florianópolis, outubro de 2004. Introdução Este tutorial introdutório aborda basicamente os 7 (dos 17) primeiros capítulos do manual de referência do Hibernate (http://www.hibernate.org) de forma resumida. O objetivo é passar as informações necessárias para que o leitor possa começar a persistir objetos e tenha base para entender os conceitos mais avançados do Hibernate. Para isso, começaremos mostrando como o Hibernate foi projetado e como funciona e depois como utilizá-lo. Mas afinal, o que é o Hibernate? De acordo com seus criadores, Hibernate é um serviço de consulta e persistência Objeto/Relacional para Java, muito poderoso e de alto desempenho. A grande maioria dos desenvolvedores de sistemas ainda opta por utilizar bancos de dados relacionais pela confiabilidade e robustez, embora o paradigma utilizado seja o Orientado a Objetos. Dessa forma, os registros do banco de dados devem ser transformados em objetos e as informações contidas nos objetos devem ser persistidas em forma de linhas e colunas. Chamamos isso de “Mapeamento Objeto-Relacional”. Muitas vezes, os desenvolvedores acabam consumindo muito tempo de desenvolvimento para fazer este mapeamento. A proposta do Hibernate é exatamente prover aos desenvolvedores uma maneira de realizar este mapeamento de forma transparente, isto é, criando classes como se não houvesse persistência relacional. Mas o Hibernate não provê apenas simples mapeamento de uma classe para uma tabela. Relacionamentos, Herança, Polimorfismo, Composições e Coleções são algumas dos conceitos OO contemplados pelo Hibernate. Ele também possui o HQL (Hibernate Query Language), uma linguagem de consultas orientada a objetos muito interessante! Hibernate suporta uma enorme quantidade de bancos de dados, incluindo DB2, PostgreSQL, MySQL, Oracle, Sybase, SQL Server, dentre outros. E além de tudo isso, Hibernate é um software livre. Qualquer pessoa pode utilizá-lo em aplicações domésticas e comerciais de acordo com a LGPL (http://www.gnu.org/copyleft/lesser.html). Arquitetura Dependendo da complexidade do projeto, um maior número de APIs e componentes são utilizados pelo Hibernate. A figura abaixo exibe aqueles que são necessários em um sistema mais simples possível: De acordo com a especificação do Hibernate, estes componentes são definidos da seguinte forma: SessionFactory (net.sf.hibernate.SessionFactory) Armazena os mapeamentos e configurações compilados. É uma fábrica de objetos Session. Também provê conexões. Este objeto é imutável e threadsafe (pode ser acessado por múltiplas threads sem perigo de inconsistência). Session (net.sf.hibernate.Session) Um objeto que representa o "diálogo" entre a aplicação e apersistência (banco de dados, etc), encapsulando uma conexão JDBC. Este objeto não deve ser manipulado por múltiplas threads. Ele controla um cache dos objetos persistentes. Os Session não devem durar toda a execução da aplicação, ou seja, são objetos chamados "de vida curta". Persistent Objects (objetos persistentes) Objetos manipulados por somente uma thread que contém informações persistentes e lógica de negócio. Devem ser JavaBeans (possuir um construtor sem parâmetros e métodos get/set para os atributos persistidos). Eles só podem estar associados à exatamente uma Session. Transient Objects (objetos transientes) Instâncias das classes persistentes que não estão atualmente associadas a uma Session. Podem ter sido instanciados pela aplicação mas não persistidos ainda, ou recuperados por uma Session que foi fechada. Transaction (net.sf.hibernate.Transaction) Objeto usado pela aplicação para especificar unidades atômicas de acesso ao banco. Não deve ser manipulado por múltiplas threads. Abstrai a aplicação de transações JDBC, JTA ou CORBA. Uma Session pode manipular várias Transaction. Além de seus próprios componentes, o Hibernate utiliza APIs de outros projetos (como drivers JDBC do banco de dados). Por isso é preciso prover todos os JARs (bibliotecas) que o Hibernate vai precisar. Felizmente, quase todos vêem na distribuição padrão do Hibernate. A próxima seção explica como instalar o Hibernate. Instalação Instalar o Hibernate é simples. Vá ao site oficial e baixe o arquivo de instalação da última versão estável. Ao descompactá-lo, um diretório é criado. Este diretório contém o JAR núcleo do Hibernate (hibernate2.jar, para a versão 0.2.x). Também há um subdiretório chamado lib. Ali estão os JARs das outras APIs utilizadas por ele. Certifique-se de que esses JARs estejam no CLASSPATH de sua aplicação e você poderá importar as classes do Hibernate. É necessário também que a classe de driver do seu banco de dados também esteja no CLASSPATH. Como visto na seção Arquitetura, quais dessas APIs serão efetivamente utilizadas pelo Hibernate depende da complexidade do seu projeto. Configurando o SessionFactory Hibernate foi projetado para operar em diferentes ambientes. Por isso, existe um grande número de parâmetros de configuração. A maioria deles já possui valores padrão e, além disso, Hibernate vem com o arquivo hibernate.properties que mostra as várias opções. A lista abaixo contém os parâmetros mais importantes para configuração da conexão JDBC (com banco de dados). hibernate.connection.driver_class Classe do driver JDBC hibernate.connection.url URL JDBC hibernate.connection.username Usuário do banco de dados hibernate.connection.password Senha do usuário do banco de dados hibernate.connection.pool_size Número de conexões disponíveis O Hibernate possui ainda uma série de parâmetros que definem seu comportamento em tempo de excução. Nesta introdução vamos abordar apenas os dois da lista abaixo: hibernate.dialect Subclasse de net.sf.hibernate.dialect.Dialect que contém características particulares de cada banco de dados hibernate.show_sq Exibir as declarações SQL executadas no console O Hibernate possui dialetos para um grande número de bancos de dados tais como: DB2, MySQL, SAP DB, Oracle, Oracle 9, Sybase, Sybase Anywhere, Progress, Mckoi SQL, Interbase, Pointbase, PostgreSQL, HypersonicSQL, Microsoft SQL Server, Ingres, Informix e FrontBase. Vamos usar como exemplo o banco de dados MySQL, cuja classe é net.sf.hibernate.dialect.MySQLDialect. As classes dos outros bancos de dados estão descritas no manual de referência do Hibernate. Estes parâmetros são usados para configuração inicial. A partir deles, é criado um objeto da classe SessionFactory. Este objeto contém todas as informações passadas na configuração. Após sua criação, as configurações podem ser descartadas. Quando criado, o SessionFactory carrega (e avalia) todas as configurações. Esta operação é a que despende mais tempo no Hibernate. Por isso, é recomendável criar este objeto somente uma vez e utilizá-lo durante toda execução da aplicação. O Hibernate permite que sejam criados mais de um SessionFactory, mas isso só deve ser feito se houver necessidade de acesso a mais de um banco de dados. Existem duas formas de informar ao SessionFactory as configurações do Hibernate. Configuração através do objeto Configuration A configuração pode ser feita através da classe Configuration (net.sf.hibernate.cfg.Configuration). Os objetos desta classe podem ser instanciados de forma direta. Configuration deve receber as configurações gerais (através da classe Properties) e também os mapeamentos Objeto/Relacional (apresentados posteriormente). Veja abaixo um exemplo de como criar um SessionFactory através do objeto Configuration: Configuration cfg = new Configuration() .setProperty("hibernate.connection.driver_class", "com.mysql.jdbc.Driver") .setProperty("hibernate.connection.url", "jdbc:mysql://127.0.0.1/NomeDoBanco") .setProperty("hibernate.connection.username","usuario") .setProperty("hibernate.connection.password","senha") .setProperty("show_sql","true") .setProperty("dialect","net.sf.hibernate.dialect.MySQLDialect") .addFile("MapeamentoObjetoRelacional.hbm.xml"); //Apresentado posteriormente try{ SessionFactory sf = cfg.buildSessionFactory(); }catch(HibernateException he){ he.printStackTrace(); } Configuração através do arquivo hibernate.cfg.xml Uma outra forma (talvez a mais utilizada) de criar o SessionFactory é através de um arquivo de configuração XML chamado hibernate.cfg.xml, como no exemplo abaixo: <?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" "http://hibernate.sourceforge.net/hibernate-configuration-2.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.driver_class"> com.mysql.jdbc.Driver</property> <property name="hibernate.connection.url"> jdbc:mysql://127.0.0.1/NomeDoBanco</property> <property name="hibernate.connection.username">usuario</property> <property name="hibernate.connection.password">senha</property> <property name="show_sql">true</property> <property name="dialect"> net.sf.hibernate.dialect.MySQLDialect</property> <!--Apresentado Posteriormente--> <mapping resource="MapeamentoObjetoRelacional.hbm.xml"/> </session-factory> </hibernate-configuration> Este arquivo de configuração deve ser, então, processado para criar um objeto Configuration. O objeto resultante é utilizado para criar o SessionFactory. Se o arquivo estiver no CLASSPATH não é necessário passar nenhum parâmetro para o método configure (neste caso o nome do arquivo tem que ser hibernate.cfg.xml), caso contrário o caminho deve ser especificado (e seu nome é livre): try{ Configuration cfg = new Configuration().configure("/engenho/hibernate.cfg.xml"); SessionFactory sf = cfg.buildSessionFactory(); }catch(HibernateException he){ he.printStackTrace(); } Mapeamento O/R Básico O Hibernate usa arquivos XML para mapear os atributos das classes em campos das tabelas. Qualquer classe pode ser mapeada desde que seja um Bean, ou seja, possua um construtor sem parâmetros e os atributos mapeados possuam métodos get e set. A presença de um atributo de identificação (chave primária) é altamente recomendada, mas não é obrigatória, caso contrário diversas funcionalidades não estarão disponíveis. Não é necessário nenhum tipo de especialização (herança) ou realização de interface para que uma classe seja persistida pelo Hibernate. Isto quer dizer que o Hibernate pode persistir objetos POJO (Plain Old Java Object). Veja abaixo o exemplo de uma classe que poderia ser persistida: Abaixo, a implementação da classe do diagrama UML acima. package engenho; import java.util.Date; public class Pessoa { private Long idPessoa; private String nome; private Date dataNascimento; public Pessoa() { } public Long getIdPessoa() { return idPessoa; } public void setIdPessoa(Long idPessoa) { this.idPessoa = idPessoa; } public String getNome() {return nome;} public void setNome(String nome) { this.nome = nome; } public Date getDataNascimento() { return dataNascimento; } public void setDataNascimento(Date dataNascimento){ this.dataNascimento = dataNascimento; } } Veja a declaração de criação da tabela que armazena a classe acima. Aconselha-se escolher identificadores de tipos de grande capacidade (como BIGINT) para que um grande número de registros possa ser armazenado. Note o atributo AUTO_INCREMENT para a chave primária idPessoa, logo em seguida fica claro o porquê disso. CREATE TABLE Pessoa ( idPessoa BIGINT NOT NULL AUTO_INCREMENT, nome VARCHAR(50) NOT NULL, dataNascimento DATETIME NULL, PRIMARY KEY(idPessoa) ) TYPE=InnoDB; Para mapear uma classe numa tabela: <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 2.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-2.0.dtd"> <hibernate-mapping> <class name="engenho.Pessoa" table="Pessoa"> <id name="idPessoa"> <generator class="identity" /> </id> <property name="nome"/> <property name="dataNascimento"/> </class> </hibernate-mapping> Os arquivos de mapeamento são fáceis de configurar e divididos de maneira bastante intuitiva. Além disso, a maioria dos parâmetros existentes possui valores padrão. Este exemplo inicial exibe apenas os parâmetros obrigatórios (com exceção do elemento <id>). É obrigatório especificar o nome da classe e a tabela com a qual está associada. O elemento <id> indica qual é o identificador do objeto e como ele é criado e obtido. Caso ele seja omitido, o Hibernate considera que a classe não possui identificador. O atributo name indica que atributo da classe será usado como identificador. O elemento generator informa como serão gerados os identificadores dos novos elementos. Veja alguns dos tipos de geradores existentes no Hibernate: increment - Gera identificadores dos tipos long, short ou int que são únicos apenas quando nenhum outro processo está inserindo na mesma tabela. Não deve ser usado em clusters identity usado para colunas identity nos bancos DB2, MySQL, MS SQL Server, Sybase e HypersonicSQL. O identificador retornado é do tipo long, short ou int sequence Usado para sequence nos bancos DB2, PostgreSQL, Oracle, SAP DB, McKoi ou em um generator no banco Interbase. O identificador retornado é do tipo long, short ou int hilo Utiliza o algorítmo "High / Low" para geração eficiente de identificadores de objeto do tipo long, short ou int, dada uma tabela e uma coluna desta tabela (por default, hibernate_unique_key e next respectivamente) como fonte de valores "high". O algorítmo gera identificadores que são únicos somente para um banco de dados em particular. assigned - Deixa que a aplicação atribua o identificador antes que os métodos de persistência (save() ou saveOrUpdate()) sejam chamados foreign - Usa o identificador de um objeto associado. Comumente usado em conjunto com uma associação um para um. Cada atributo da classe é mapeado através do elemento <property>. O único parâmetro obrigatório é o nome do atributo. O Hibernate tentará encontrar uma coluna com este mesmo nome e definir seu tipo por reflexão. Abaixo, o mapeamento desta mesma classe com os parâmetros opcionais (use o mapeamento simples para executar como exemplo, note que os nomes das colunas foram trocados). <hibernate-mapping> <class name="engenho.Pessoa" table="Pessoa" dynamic-update="true"> <id name="idPessoa" column="ID_PESSOA" type="long" unsaved-value="0"> <generator class="identity" /> </id> <property name="nome" column="NOME" type="string" not-null="true"/> <property name="dataNascimento" column="DATA_NASCIMENTO" type="java.util.Date" not-null="false"/> </class> </hibernate-mapping> O primeiro parâmetro a notar é column. Ele indica a coluna correspondente ao atributo da classe na tabela, caso não tenham o mesmo nome. O parâmetro type, indica o tipo do atributo Hibernate. Veja a associação entre os tipos do Hibernate mais comuns, as classes wrapper Java e os tipos no banco de dados: Tipo Hibernate Classe Java Tipo no Banco de Dados integer, long, short, float, double, character, byte, boolean e os tipos yes_no, true_false (ambos compatíveis com boolean Java.lang.Integer, java.lang.Long, java.lang.Short, java.lang.Float, java.lang.Double, java.lang.Character, java.lang.Byte, java.lang.Boolean e os tipos . Cada banco possui tipos específicos para estes tipos Java e Java.lang.Boolean) java.lang.Boolean e os tipos primitivos int, long, short, float, double, character, byte e Boolean string java.lang.String VARCHAR (VARCHAR2 para Oracle) date, time, timestamp java.util.Date DATE, TIME e TIMESTAMP (ou equivalentes) calendar, calendar_date java.util.Calendar TIMESTAMP e DATE (ou equivalentes) big_decimal java.math.BigDecimal NUMERIC (NUMBER para Oracle) locale, timezone, currency java.util.Locale, java.util.TimeZone e java.util.Currency VARCHAR (VARCHAR2 para Oracle) class java.lang.Class VARCHAR (VARCHAR2 para Oracle) binary Array de bytes Tipo binário específico de cada banco text java.lang.String CLOB ou TEXT serializable Qualquer classe serializável Tipo binário específico de cada banco clob, blob java.sql.Clob e java.sql.Blob. CLOB e BLOB No mapeamento, pode ser informado na propriedade type tanto o tipo Hibernate quando a classe Java. Outra propriedade opcional importante é not-null. Ela indica que no momento da persistência de um objeto, o determinado atributo pode ser nulo ou não. Caso um atributo esteja indicado como not-null=”true” e no momento do salvamento ele esteja null, Hibernate irá lançar uma exceção. Lembre-se de incluir a linha abaixo no arquivo do hibernate.cfg.xml. Ela indica que este mapeamento deve estar contemplado no SessionFactory. <mapping resource="engenho/Pessoa.hbm.xml"/> Manipulando objetos persistentes Para persistir e recuperar os objetos, o Hibernate provê objetos chamados Session. Session pode ser considerado uma sessão de comunicação com o banco de dados através de uma conexão JDBC. Hibernate lhe permite, mas não é recomendável, manipular suas próprias conexões. O exemplo abaixo demonstra a criação e persistência de um objeto do tipo Pessoa. try{ Session s = sf.openSession(); Pessoa novaPessoa = new Pessoa(); novaPessoa.setNome("Bruno"); Calendar c = Calendar.getInstance(); c.set(1981, 4, 6); novaPessoa.setDataNascimento(c.getTime()); s.saveOrUpdate(novaPessoa); s.connection().commit(); s.close(); }catch(HibernateException he){ he.printStackTrace(); }catch(SQLException sqle){ sqle.printStackTrace(); } O método saveOrUpdate(), apresentado no código acima, pode ser usado tanto para inserções quanto para atualizações. A chamada a esse método indica à Session que o estado do objeto passado como parâmetro deve ser persistido. Se o método commit() da connection associada à Session não for chamado, as alterações não serão efetivadas no banco de dados. Existe ainda um método chamado flush(). Este é útil para enviar imediatamente ao banco de dados todas as declarações de manipulação da Session (por exemplo, um INSERT caso um objeto tenha sido inserido). Este método é chamado automaticamente a cada commmit(). O Hibernate disponibiliza ainda um objeto Transaction (net.sf.hibernate.Transaction) para controle das modificações. Veja as possibilidades no trecho de código abaixo: Session s = null; Transaction t = null; try{ s = sf.openSession(); Pessoa pessoa1 = (Pessoa) s.load(Pessoa.class, new Long(1)); Pessoa pessoa2 = (Pessoa) s.load(Pessoa.class, new Long(2)); t = s.beginTransaction(); pessoa1.setNome("Marta"); s.flush(); pessoa2.setNome("Penha"); s.flush(); t.commit(); }catch(HibernateException he){ t.rollback(); }finally{ s.close(); } Enquanto SessionFactory é pesado e lento para criar, os objetos Session são leves e rápidos. O exemplo anterior já apresenta o método load(), que permite recuperar um único objeto do banco através de seu identificador. Entretanto, muitas vezes queremos obter uma lista dos objetos existentes. Isto pode ser feito através do método find(). Session s = sf.openSession(); List pessoas = s.find("from Pessoa"); for(int indice=0; indice<pessoas.size(); indice++){ Pessoa pessoa = (Pessoa)pessoas.get(indice); System.out.println(pessoa.getNome()); } s.close(); O método find(), apresentado acima, recebe como parâmetro uma String que deve ser a consulta HQL (Hibernate Query Language). Essa linguagem é muito semelhante ao SQL (Structured Query Language), mas é 100% orientada a objetos. A HQL será abordada nos futuros tutoriais. Conclusão O Hibernate consegue combinar duas características que se almeja em praticamente todos os softwares: Simplicidade e Robustez. Este tutorial apresentou conceitos que permitem ao leitor iniciar o uso e aprendizado do Hibernate de forma clara e direta. Para quem deseja saber mais, consulte o manual de referência do Hibernate e os fóruns de discussão na Internet.