JPA 2.0 – Persistência a toda prova

O artigo aborda as inovações realizadas na API JPA (Java Persistence API) em sua versão 2.0, lançada em dezembro de 2009 juntamente com o Java EE 6. As novidades discutidas no artigo são caracterizadas como recursos de grande importância para a adoção dessa API especialmente em sistemas legados.

Para que serve:

O objetivo do artigo é divulgar as principais mudanças realizadas na API JPA 2.0 focando, principalmente, nos problemas encontrados pelos desenvolvedores que usaram ou tentaram usar a versão 1.0 da API.

Em que situação o tema útil:

O tema é relevante tanto para usuários da API JPA 1.0 quanto para quem pretende usar um framework de persistência de dados em suas aplicações. Para quem já usa JPA, a versão 2.0 oferece recursos que aumentam muito a flexibilidade da modelagem objeto-relacional, valendo realmente a pena a migração para essa versão. Para quem deseja optar por um framework, a JPA 2.0 se apresenta como uma API madura que segue o rigor das especificações da plataforma Java e atende a maioria das necessidades de persistência dos sistemas novos e legados.

JPA 2.0 – Persistência a toda prova:

Desde seu lançamento em 2006, a API de persistência da plataforma Java – JPA (Java Persistence API) – sempre recebeu diversas críticas sobre as capacidades de mapeamento objeto-relacional oferecidas pela API em comparação com o poder dos frameworks de persistência já conhecidos do mercado. Apesar de ter tido boa aceitação (exatamente por ser um dos padrões da plataforma Java), usuários de JPA tiveram que recorrer muitas vezes aos recursos particulares dos frameworks para alcançar os resultados esperados, especialmente no mapeamento de objetos para sistemas legados. Para eliminar de vez tais limitações, em dezembro de 2009 foi lançada a JPA 2.0 que trouxe inúmeros novos recursos para atender a maioria das necessidades de mapeamento objeto-relacional dos sistemas. Neste artigo conheceremos as principais mudanças tanto nos recursos de mapeamento, quanto nas novas APIs e na linguagem de consulta JPQL.

O lançamento da JPA – Java Persistence API (versão 1.0), em Maio de 2006, representou um passo importante da plataforma Java em relação à persistência de dados (veja o quadro “JPA 1.0”). A API JPA é utilizada nas aplicações Java para permitir a gravação/leitura de objetos em bancos de dados relacionais de forma transparente. Essa técnica é conhecida como ORM (Object Relational Mapping) e já era utilizada por frameworks de persistência proprietários como Hibernate, TopLink, Kodo, entre outros.

Se compararmos o tempo de existência dos frameworks de persistência com a JPA, vamos perceber que essa API demorou para ser criada na plataforma Java. Mas essa demora acabou por trazer alguns benefícios, entre eles, a capacidade de utilizarmos implementações de diferentes fornecedores, não ficando presos a um framework em particular, e a flexibilidade de utilizarmos esses recursos de persistência tanto em aplicaçõesque rodam dentro de servidores quanto em aplicações stand-alone.

Desde seu lançamento, muitas empresas, com visão de futuro, foram motivadas a migrar suas aplicações para JPA e acabaram enfrentando alguns problemas. Por ser a primeira versão, o principal objetivo da especificação de JPA foi o de disponibilizar os recursos básicos necessários para a persistência de dados. O resultado foi que cerca de 10 a 20% dos recursos dos frameworks já existentes não foram inseridos na versão 1.0. Isso obrigou as pessoas a utilizarem a API JPA combinada com outros frameworks de persistência que já possuíam recursos diferenciados para o mapeamento de objetos, principalmente para bancos de dados legados.

Na versão 2.0, lançada em Dezembro de 2009 juntamente com o Java EE 6, a API JPA surge muito mais madura. Além de reduzir as restrições para oferecer maior flexibilidade no desenvolvimento do modelo de objetos, a API incorporou a maioria dos recursos existentes nos frameworks proprietários que faziam falta na versão 1.0, sendo que grande parte das melhorias foram feitas para atender às necessidades de mapeamento dos sistemas legados. Tais mudanças objetivaram principalmente a possibilidade das aplicações Java se tornarem mais independentes de frameworks proprietários.

JPA 2.0 foi especificada pela JSR 317 do Java EE 6 e sua implementação de referência é o EclipseLink, disponibilizado no servidor GlassFish V3 (veja o quadro “TopLink x EclipseLink”). Por ser considerada uma das especificações mais maduras do Java EE 6, seus recursos já podem ser utilizados nas aplicações Java, desde que a implementação JPA utilizada já suporte essa nova especificação.

Neste artigo vamos abordar as principais mudanças na API JPA 2.0, em particular, as melhorias realizadas no modelo de mapeamento objeto-relacional, os novos recursos adicionados à JPA Query Language e as APIs de MetaModel e Criteria.

JPA 1.0

JPA é utilizado nas aplicações Java para permitir o armazenamento de dados em bancos de dados relacionais sem a necessidade de escrever código JDBC (Java Database Connectivity), utilizar componentes EJB Entity Beans ou ficar preso a um framework de persistência proprietário

Por meio de JPA, o programador delega as operações de manipulação de tabelas para um framework de persistência que implementa a API JPA.

Utilizando recursos de ORM (Object Relational Mapping), o programador cria mapeamentos das classes Java e seus relacionamentos para as tabelas do banco de dados. Esses mapeamentos permitem que um framework, que suporte JPA (como Hibernate, Oracle TopLink, Kodo, OpenJPA, entre outros), faça as devidas inserções, buscas, exclusões e alterações nos dados da aplicação nas tabelas do banco de dados, quando for solicitado. Dessa forma, o código da aplicação fica independente de frameworks de persistência, pois todo o código utilizado para manipular o banco de dados passa a ser da API JPA.

A grande vantagem dessa API é que ela permite a troca do framework de persistência, e consequentemente do sistema de gerenciamento de banco de dados (SGBD), de forma transparente para a aplicação, ou seja, podemos optar por soluções de implementação diferentes de acordo com o perfil do projeto ou do banco de dados.

TopLink x EclipseLink

Para o JPA 1.0, a Oracle oferece o Oracle TopLink (versão comercial), e o Oracle TopLink Essentials, que é a implementação de referência da JPA 1.0.

O EclipseLink corresponde à parte “open source” do código desenvolvido para a versão 11g do Oracle TopLink. Isso significa que o EclipseLink substituiu o produto “Oracle TopLink Essentials” e também se tornou a implementação de referência da versão JPA 2.0, sendo distribuído juntamente com o servidor de aplicações GlassFish V3.

Aos poucos todos os recursos desenvolvidos para o Oracle TopLink estão sendo convertidos para o EclipseLink.

Mapeamento Objeto-Relacional

Sem dúvida alguma, as questões relacionadas ao mapeamento objeto-relacional foram as que mais sofreram melhorias na versão 2.0 da JPA.

Além de melhorar alguns mapeamentos que eram limitados na JPA 1.0, a nova versão criou outros recursos de mapeamento com base no que já existia nos frameworks de persistência proprietários como Hibernate, visando atender às necessidades da maioria dos usuários.

A seguir apresentamos alguns dos principais recursos de mapeamento OR que foram disponibilizados na JPA 2.0.

Coleções de tipos básicos e embeddable

Na JPA 1.0, quando uma entidade tem um relacionamento 1:N ou N:N com outras entidades, podemos usar coleções para representar o relacionamento e mapear essas coleções para o banco de dados usando as anotações @ManyToMany ou @OneToMany.

No entanto, além das coleções que representam relacionamentos entre as entidades, é possível que uma classe também possua coleções de tipos básicos, como coleções de String, Integer, Float, etc., e esse tipo de coleção não pode ser mapeado com JPA 1.0.

Com JPA 2.0, podemos fazer o mapeamento de coleções de tipos básicos usando as anotações @ElementCollection e @CollectionTable.

A anotação @ElementCollection permite definir que os elementos da coleção de tipos básicos devem ser armazenados numa tabela à parte da tabela da entidade, por exemplo:

@Entity @Table(name=“pessoas”) class Pessoa{ @Id private long id; private String nome; @ElementCollection private Collection<String> apelidos; … }

Nesse exemplo, os apelidos de uma pessoa devem ser armazenados em uma tabela separada da tabela de pessoas. Essa tabela extra armazena uma String para o apelido e uma chave indicando de qual pessoa é aquele apelido. Como JPA trabalha com configurações default, o nome dessa tabela (Pessoa_apelidos) é definido automaticamente em função do nome da entidade Pessoa e do atributo apelidos. Mas se quisermos controlar o nome da tabela e o nome da coluna que vai armazenar o apelido, podemos usar as anotações @CollectionTable e @Column, como no exemplo a seguir:

@Entity @Table(name=“pessoas”) class Pessoa{ @Id private long id; private String nome; @ElementCollection @CollectionTable(name=“apelidos”) @Column(name=“apelido_pessoa”) Collection<String> apelidos; }

Agora o nome da tabela onde os apelidos da pessoa são armazenados passa a ser “apelidos”, e a coluna do apelido nessa tabela passa a se chamar “apelido_pessoa”. As tabelas utilizadas nesse mapeamento podem ser vistas na Figura 1.


Figura 1. Mapeamento de coleções de tipos básicos.

Além das coleções de tipos básicos, JPA 2.0 também suporta coleções de tipos embeddable. Tipos embeddable são classes com atributos que precisam ser persistidos mas não são consideradas entidades, pois seus objetos dependem de uma outra entidade, ou seja, não fazem sentido sozinhos.

Com JPA 1.0, podemos mapear um tipo embeddable de forma que os atributos dessa classe sejam armazenados na mesma tabela da entidade que o utiliza. Por exemplo:

@Embeddable public class Endereco { private String rua; private int numero; ... } @Entity @Table(name=“clientes”) public class Cliente { @Id private long id; private String nome; @Embedded private Endereco endereco; … }

Nesse mapeamento, a tabela “clientes” deve possuir colunas para armazenar o id e o nome do cliente, e também a rua e o numero do endereço. Ou seja, além dos dados da entidade Cliente, os dados do embeddable Endereco também são armazenados na tabela “clientes”.

Com JPA 2.0, podemos criar coleções de tipos embeddable e mapeá-las usando a anotação @ElementCollection, assim como fazemos com as coleções de tipos básicos. O exemplo a seguir ilustra esse mapeamento:

@Entity @Table(name = “clientes”) public class Cliente { @Id private long id; private String nome; @ElementCollection @CollectionTable(name=“cliente_enderecos”) private Collection<Endereco> endereco; … }

Assim, a tabela “clientes” guarda apenas o id e nome do cliente, enquanto a tabela “cliente_enderecos” guarda rua, numero e a chave para o cliente. Veja na Figura 2 as tabelas necessárias para esse mapeamento.


Figura 2. Mapeamento de coleções de tipos embeddable.

Para customizar também o nome das colunas da tabela da coleção, podemos usar as anotações @AttributeOverrides e @AttributeOverride. O trecho de código da Listagem 1 define o nome das colunas a serem utilizadas para os atributos rua e numero:

Listagem 1. Mapeamento da coleção de embeddable com customização nos nomes das colunas.

@ElementCollection @CollectionTable(name = “cliente_enderecos”) @AttributeOverrides(value = { @AttributeOverride(name = “rua”, column = @Column(name = “END_RUA”)), @AttributeOverride(name = “numero”, column = @Column(name = “END_NUMERO”)) }) private Collection<Endereco> enderecos;

Listas ordenadas

Para representar o relacionamento 1:N ou N:M, podemos definir os atributos com os tipos Collection, Set, List ou Map. A escolha do tipo de coleção depende das características individuais de cada tipo e das necessidades do projeto em questão. Por exemplo, se usarmos uma coleção de tipo Set, a característica mais evidente é que ela não aceita elementos duplicados. Já a coleção List trabalha com o conceito de índices, enquanto Map usa pares de chave e valor.

No caso especial do tipo List, até a JPA 1.0, podíamos usar a anotação @OrderBy(“nomeAtributo ASC/DESC”) para definir a ordem que os objetos armazenados deveriam ser apresentados na coleção. Ao usar essa anotação, a JPA se encarrega de ordenar os elementos na coleção List quando os objetos são recuperados do banco de dados para a memória. No entanto, se reorganizarmos essa ordem, ela não é persistida para o banco de dados, já que ela é estabelecida em função de atributos da classe associada.

Com JPA 2.0, podemos ordenar uma lista de objetos como quisermos na memória e, depois, persistir essa ordem no banco de dados para futuras recuperações. Para isso, podemos usar a anotação @OrderColumn com mapeamentos @OneToMany ou @ManyToMany, da seguinte forma:

@Entity @Table(name = “cursos”) public class Curso { @Id private long id; private String nome; @OneToMany @OrderColumn(name=“ordem_matricula”) List<Matricula> matriculas;... }

As tabelas desse mapeamento são ilustradas na Figura 3. Aqui, a tabela que armazena as matrículas do curso tem uma coluna a mais chamada “ordem_matricula”. Essa coluna armazena a posição (índice) de cada elemento da lista de matrículas. Dessa forma, sempre que recuperarmos as matrículas de um curso, a ordem estabelecida na lista é mantida.


Figura 3. Mapeamento de listas ordenadas.

Mapeamento de Maps

O uso de coleções de tipo Map (coleção de pares chave-valor) é bastante restrito na JPA 1.0 e, por isso, esse foi um dos recursos de mapeamento que mais foram aprimorados na nova especificação.

Na JPA 1.0, o mapeamento de mapas só é possível se os valores do Map forem entidades e as chaves forem as chaves primárias dos objetos valores.

Na JPA 2.0, os mapas podem ter chaves e valores de tipos básicos, embeddable ou entidades. Para suportar essas alterações, novas anotações foram criadas, tais como:

  • @MapKeyColumn e @MapKeyJoinColumn, para especificar o nome da coluna da chave quando ela é de tipo básico e entidade, respectivamente;
  • @MapKeyClass, para definir o tipo da chave em mapas não genéricos;
  • @MapKeyTemporal, para chaves de tipo Date;
  • @MapKeyEnumerated, para enumerar as chaves possíveis previamente.

Quando os valores do Map são entidades, podemos usar as anotações @ManyToMany ou @OneToMany para mapear o relacionamento. Se os valores são tipos básicos ou embeddable, devemos usar @ElementCollection, como fazemos com as coleções Set e List.

A organização das tabelas do banco de dados para atender ao mapeamento de coleções Map depende diretamente dos tipos das chaves e valores armazenados. Por isso, considerando que há três tipos de chaves e três tipos de valores (que são entidades, tipos básicos e tipos embeddable), são nove combinações possíveis de mapeamento de Map.

Neste artigo, optamos por exemplificar o uso de Map com apenas dois dos casos mais comuns. Para ver outras formas de mapeamento, aconselhamos a leitura da especificação ou do livro referenciado ao final deste artigo.

Veja na Listagem 2 um exemplo de mapeamento Map<String, String>. A Figura 4 ilustra as tabelas desse relacionamento.

Listagem 2. Mapeamento de coleção Map<String, String>.

@Entity @Table(name=“empregados”) public class Empregado { @Id private long id; String nome; @ElementCollection @CollectionTable(name=“empregados_telefones”) @MapKeyColumn(name=“tipo_telefone”) @Column(name=“numero_telefone”) private Map<String, String> numeroTelefones; ... }

Figura 4. Mapeamento de Map <String, String>.

Nesse exemplo, como os valores do Map são de tipo String (tipo básico), mapeamos a coleção com @ElementCollection, e usamos @CollectionTable para definir o nome da tabela da coleção. Como as chaves do Map também são de tipo String, usamos a anotação @MapKeyColumn para definir o nome da coluna que vai armazenar a chave. A anotação @Column define o nome da coluna para o valor do Map. Repare nas tabelas que, para esse caso, a chave do Map fica armazenada dentro da tabela da coleção definida com a anotação @CollectionTable.

Se mudarmos o tipo de valor do Map, temos um outro caso de mapeamento. O exemplo da Listagem 3 ilustra o mapeamento de um Map<String, Empregado>. As tabelas são apresentadas na Figura 5.

Listagem 3. Mapeamento de coleção Map<String, Empregado>.

@Entity @Table(name=“departamentos”) class Departamento { @Id private long id; private String nome; @OneToMany(mappedBy=“departamento”) @MapKeyColumn(name=“sala”) private Map<String, Empregado> empregados; ... } @Entity @Table(name=“empregados”) public class Empregado { @Id private long id; private String nome; @ManyToOne private Departamento departamento; ... }

Nesse exemplo, as chaves do Map continuam sendo de tipo básico (String), mas o objeto valor é uma entidade (Empregado). Conforme dissemos anteriormente, o objeto valor define que tipo de anotação podemos utilizar: quando o valor é um tipo básico, mapeamos com @ElementCollection (exemplo anterior); e quando é uma entidade, usamos @OneToMany ou @ManyToMany. Nesse exemplo, temos um relacionamento 1:N porque um departamento tem muitos empregados, por isso, usamos @OneToMany. Chaves de tipos básicos permitem usar a anotação @MapKeyColumn que, nesse caso, é armazenada dentro da tabela dos objetos valores.


Figura 5. Mapeamento de Map<String, Empregado>.

Se o nosso modelo de dados tivesse o relacionamento N:M, ou seja, um departamento tem muitos empregados e um empregado pode estar em vários departamentos, o relacionamento com o Map deveria usar @ManyToMany e @JoinColumn para a tabela de junção. Nesse caso, o @MapKeyColumn estaria definindo a chave dentro da tabela de junção.

Exclusão de objetos órfãos

Em relacionamentos @OneToMany e @OneToOne trabalhamos com o conceito de objetos pais (aqueles que contêm) e filhos (aqueles que são contidos). Em muitos casos, os objetos filhos não podem existir sozinhos e nem serem associados a outros objetos pais. Por exemplo, a avaliação de um aluno não pode existir sem o aluno e também não pode ser associada a outro aluno. Isso significa que se o relacionamento entre esses objetos for “quebrado” por algum motivo, por exemplo, uma avaliação é desassociada do aluno, o objeto da avaliação (objeto filho) precisa ser excluído, caso contrário ele se tornará um objeto órfão.

O suporte de JPA 1.0 para evitar objetos órfãos está limitado ao recurso de cascade: quando o relacionamento suporta CascadeType.REMOVE, ao remover o objeto pai, os objetos filhos também são excluídos. Mas se quisermos excluir um (ou mais) dos objetivos filhos, temos que “quebrar” o relacionamento e excluir o objeto filho manualmente.

Na JPA 2.0, podemos simplesmente adicionar um atributo orphanRemoval = true nos relacionamentos @OneToMany e @OneToOne quando quisermos que o objeto filho seja automaticamente excluído quando o seu relacionamento com o objeto pai for “quebrado”.

Este recurso também é útil quando removemos o objeto pai. Mesmo quando não estamos usando CascadeType.REMOVE, a presença do atributo orphanRemoval faz com que as entidades filhas sejam excluídas ao excluir o objeto pai.

Na Listagem 4 ilustramos o relacionamento @OneToMany bidirecional usando o atributo orphanRemoval.

Listagem 4. Mapeamento 1:N com orphanRemoval.

@Entity @Table(name=“funcionarios”) public class Funcionario { @Id private long id; @OneToMany(mappedBy = “funcionario”, cascade = CascadeType.ALL, orphanRemoval = true) private Collection<Atividade> atividades;… } @Entity @Table(name=“atividades”) public class Atividade { @Id private long id; @ManyToOne @JoinColumn(name = “funcionario_id”) private Funcionario funcionario; … }

Usando o atributo orphanRemoval, podemos excluir uma atividade do funcionário apenas removendo-a da coleção de atividades, ou seja, muito mais simples do que retirar o objeto da coleção e ainda ter que excluí-lo manualmente. O trecho de código da Listagem 5 ilustra esse processo (considere que em é um objeto do EntityManager).

Listagem 5. Trecho de código que exclui objeto órfão automaticamente.

Funcionario f = em.find(Funcionario.class, new Long(1)); Atividade extra = null; for (Atividade e : f.getAtividades()) { if (e.getMinutos()==10) extra = e; } em.getTransaction().begin(); f.removeAtividade(extra); em.getTransaction().commit();

Combinação de tipos de acesso

Na JPA 1.0, os dados da entidade podem ser acessados de duas formas diferentes: via atributos, quando usamos anotações sobre os atributos da entidade; ou via métodos @getters, quando as anotações são feitas sobre esses métodos. Em tempo de execução, a JPA verifica onde está a anotação da chave primária (no atributo ou método anotado com @Id) e usa essa forma de acesso para os demais atributos da classe. Somente uma das formas de acesso é válida, o que significa que se usarmos acesso via atributo e, para algum dado em particular, fizermos a anotação no método, essa anotação é simplesmente ignorada.

Apesar de ser de pouco conhecimento dos programadores, a performance de acesso aos dados das entidades é melhor quando é feita via atributos ao invés de métodos. Mas eventualmente, precisamos que a JPA acesse os métodos para permitir a realização de validações ou formatações antes de enviar os dados para o banco de dados. Se isso for necessário, devemos usar a forma de acesso via métodos em toda a classe.

Com JPA 2.0, é possível mesclar as duas formas de acesso na mesma entidade, isto é, podemos utilizar anotações em atributos e em métodos de acordo com nossas necessidades.

Para exemplificar, vamos mapear uma classe Pessoa com os atributos nome e id, sendo que o atributo nome deve ser armazenado com caracteres maiúsculos. Para ter melhor performance, vamos optar pelo acesso via atributos, mas como o atributo nome tem uma regra de negócios vinculada, teremos que usar o acesso via método @getter para esse campo, visando garantir que a regra será executada ao salvar os dados no banco de dados.

Para utilizar este recurso de mapeamento devemos realizar três passos:

1. Definir junto à entidade qual é o padrão de acesso para a classe. Se escolhermos atributos, devemos anotar a classe com @Access(AccessType.FIELD), e se escolhermos métodos, a anotação deve ser @Access(AccessType.PROPERTY).

@Entity @Access(AccessType.FIELD) public class Pessoa { ... }

2. Anotar o atributo nome com @Transient para que JPA não considere esse atributo nas operações de persistência, já que optamos pelo acesso via atributos (field) por default.

@Transient private String nome;

3. Anotar o método getNome() com @Access(AccessType.PROPERTY) para que a JPA utilize esse método como mapeamento para a coluna nome nas operações de persistência.

@Column(name=“nome”) @Access(AccessType.PROPERTY) public String getNome() { return nome == null ? “” : nome.toUpperCase(); }

Assim, JPA utiliza acesso via atributos para os dados da entidade Pessoa, mas usa o método getNome() para a propriedade nome, dando mais flexibilidade ao modelo.

Este recurso pode auxiliar na performance, mas sobretudo nos mapeamentos de classes embutidas que podem ter acessos diferentes do padrão das classes que as utilizam. O mesmo vale para classes filhas que possuem formas de acesso diferentes das classes mães.

Mapeamento @OneToMany unidirecional com chave-estrangeira

Uma das principais reclamações dos usuários de JPA 1.0 que fizeram mapeamento para banco de dados legado foi o caso dos relacionamentos @OneToMany unidirecional.

Com JPA 1.0, a única forma de mapear esse relacionamento é usando uma tabela de junção, mas o comum nos sistemas legados é usar chave estrangeira. Essa deficiência de JPA leva o programador à necessidade de reorganizar as tabelas (e os dados) para criar a tabela de junção exigida no mapeamento, ou de recorrer a algum recurso particular do framework de persistência utilizado.

Para eliminar esse problema, JPA 2.0 passou a permitir o relacionamento @OneToMany unidirecional usando chave estrangeira também. No exemplo a seguir, a classe Curso tem um relacionamento 1:N unidirecional com a classe Objetivo, portanto a tabela de objetivos é quem armazena a chave para o curso (sem tabela de junção). Podemos fazer esse mapeamento usando @OneToMany na coleção de objetivos do curso e @JoinColumn para estabelecer a coluna que irá guardar a chave do curso na tabela de objetivos (ver Listagem 6). As tabelas para esse mapeamento podem ser vistas na Figura 6.

Listagem 6. Mapeamento 1:N unidirecional usando chave estrangeira.

@Entity @Table(name=“objetivos”) public class Objetivo { @Id private long id; private String titulo; private String texto; ... } @Entity @Table(name=“cursos”) public class Curso { @Id private long id; private String nome; ... @OneToMany @JoinColumn(name=“curso_id”) Set<Objetivo> objetivos; }

Figura 6. Mapeamento @OneToMany unidirecional com chave estrangeira.

@OneToOne e @ManyToOne com suporte à tabela de junção

Na mesma linha do problema descrito na seção anterior, o mapeamento JPA para bases de dados legadas também apresenta dificuldades com relacionamentos @OneToOne e @ManyToOne. Em geral, esse tipo de relacionamento pode ser resolvido usando apenas chaves estrangeiras, mas há casos nos sistemas legados onde são utilizadas tabelas de junção.

Para contornar esse problema, JPA 2.0 passou a permitir o uso da anotação @JoinTable também em mapeamentos @OneToOne e @ManyToOne unidirecional e bidirecional.

JPA Query Language

Para fazer consultas com JPA, podemos usar a JPA Query Language (JPQL). Nessa linguagem, as instruções de consulta são escritas de forma muito semelhante ao padrão SQL (Structured Query Language), incluindo palavras como select, from, where, etc., porém são baseadas nos conceitos de orientação a objetos. A implementação JPA utilizada transforma essas instruções JPQL em instruções SQL para que elas possam ser executadas no banco de dados. A vantagem em usar JPQL ao invés de SQL, que também é permitido via JPA, é que evitamos a transição entre objeto e modelo relacional, que é responsabilidade do framework de persistência.

Na JPA 2.0, a Query Language foi atualizada para suportar os novos recursos de mapeamento listados anteriormente, oferecendo mais flexibilidade nas consultas e, principalmente, facilitando o trabalho com polimorfismo, que era uma das suas principais deficiências na versão 1.0. Detalhes sobre as mudanças mais significativas são listados a seguir.

Polimorfismo restrito

Dado um modelo de herança representado por uma classe Treinamento e suas subclasses: Core, HandsOn e Carreira, se quisermos recuperar todos os treinamentos sem especificar seus tipos usando JPA 1.0, podemos utilizar a query:

SELECT t FROM Treinamentos t

Se quisermos recuperar somente treinamentos de tipo Core, podemos usar:

SELECT c FROM Core c

Com JPA 2.0, podemos realizar consultas especificando um ou mais tipos de entidades usando o operador TYPE na cláusula WHERE. A query a seguir permite recuperar somente objetos dos tipos Core e HandsOn:

SELECT t FROM Treinamentos t WHERE TYPE(t) IN (Core, HandsOn)

Suporte ao Case

JPA 2.0 oferece o operador CASE para permitir a inclusão de lógica condicional à instrução de acesso ao banco de dados. Usando esse operador podemos montar consultas e atualizações baseadas em condições.

Reconsiderando a hierarquia de classes Treinamento, Core, HandsOn e Carreira da seção anterior, podemos usar o operador CASE para retornar o nome e o tipo do curso com a query apresentada na Listagem 7.

Listagem 7. Exemplo de uso do operador CASE para uma consulta.

SELECT t.nome, CASE WHEN TYPE(t) = Core THEN "Curso Core" WHEN TYPE(t) = HandsOn THEN "Curso Hands-On" ELSE ‘Nao especificado’ END FROM Treinamento t;

Essa instrução fará com que uma lista de treinamentos seja retornada com dois valores: o nome do treinamento, considerando que há um atributo nome na classe Treinamento, e a descrição do treinamento, que pode ser “Curso Core”, “Curso Hands-On” ou “Não especificado”.

Agora, supondo uma classe Funcionario com os atributos nome (String), salario (double) e avaliacao (int), podemos fazer a atualização nos dados de um funcionário usando o operador CASE conforme o código da Listagem 8.

Listagem 8. Exemplo de uso do operador CASE para uma atualização.

UPDATE Funcionario f SET f.salario = CASE f.avaliacao WHEN 1 THEN f.salario * 1.05 WHEN 2 THEN f.salario * 1.02 ELSE f.salario * .95 END

Essa instrução permite mudar o salário de cada funcionário de acordo com o valor de suas avaliações: se for 1, o salário é multiplicado por 1.05; se for 2, o salário é multiplicado por 1.02, e para qualquer outro valor, o salário é multiplicado por 0.95.

Suporte às listas ordenadas

Com o suporte de JPA 2.0 para a criação de listas ordenadas persistentes usando a anotação @OrderColumn, a JPQL ganhou uma função chamada INDEX que permite identificar o índice de um objeto dentro da coleção ordenada. Para entender como essa função pode ser utilizada, vamos analisar o relacionamento entre as classes Curso e Estudante, sendo que o curso possui uma lista ordenada de estudantes que corresponde à lista de espera para o curso (ver Listagem 9).

Listagem 9. Mapeamento de lista ordenada de forma persistente.

@Entity public class Curso { @Id Integer id; String nome; @OrderColumn @ManyToMany List<Estudante> listaEspera; … } @Entity public class Estudante { @Id Integer id; String nome; … }

Para recuperar os nomes dos cinco primeiros estudantes da lista de espera do curso, podemos usar a função INDEX na instrução de SELECT, como exibe a Listagem 10.

Listagem 10. Consulta sobre a lista ordenada usando a função INDEX.

SELECT e.nome FROM Curso c JOIN c.listaEspera e WHERE c.nome = "Matemática" AND INDEX(e) < 5

Suporte a coleções do tipo Map

Assim como no caso das listas ordenadas, a JPQL foi ampliada para fornecer suporte ao trabalho com coleções do tipo Map com a criação dos operadores KEY, VALUE e ENTRY.

Para exemplificar, reveja o mapeamento da classe Empregado com um Map de telefones apresentado na seção “Mapeamento de Maps”. Podemos realizar a seguinte consulta para recuperar os nomes e os números de telefone de cada um dos empregados:

SELECT e.nome, tel FROM Empregado e JOIN e.numeroTelefones tel

O resultado dessa consulta é uma lista com nomes e números de telefone de cada empregado. Isso significa que, por padrão, sempre que fazemos uma busca em um Map, o resultado retornado é o valor do Map e não sua chave. Para ficar mais claro, podemos usar o operador VALUE da seguinte forma:

SELECT e.nome, VALUE(tel) FROM Empregado e JOIN e.numeroTelefones tel

Usando essa instrução podemos recuperar o mesmo resultado da consulta anterior, porém, está mais explícito que queremos o valor do Map e não sua chave. O operador KEY, por sua vez, nos permite recuperar a chave do Map, por exemplo:

SELECT e.nome, KEY(tel), VALUE(tel) FROM Empregado e JOIN e.numeroTelefones tel WHERE KEY(tel) IN ("Trabalho", "Celular")

Agora recuperamos os nomes dos empregados, os tipos e os números dos telefones caso o tipo do telefone seja “Trabalho” e/ou “Celular”. Repare que os operadores KEY e VALUE podem ser utilizados tanto com a cláusula SELECT quanto com as cláusulas WHERE e HAVING.

Se desejarmos obter o par chave-valor, podemos usar o operador ENTRY.

Criteria API

A Criteria API é, sem dúvida nenhuma, um dos recursos mais esperados pelos desenvolvedores. Com esta API podemos criar queries dinâmicas, padronizadas e efetuar a validação das mesmas em tempo de compilação. Vamos ver como funciona esta API na JPA 2.0.

Entendendo a Criteria API

Começaremos com um exemplo para entender a sintaxe e a utilização da Criteria API. Eis um exemplo de uma query com JPA 1.0, onde queremos buscar todos os clientes com o nome “Mariana”:

SELECT c FROM Cliente c WHERE c.nome = "Mariana"

A query demonstrada utiliza recursos JPQL em formato de String. Este formato de construção de queries pode acarretar em dois problemas:

1. Permite a utilização de sintaxe proprietária de alguma linguagem SQL, e nesta situação podemos perder a portabilidade entre diferentes fornecedores de bancos de dados;

2. Permite a existência de erros na sintaxe da JPQL, e neste caso o erro só será descoberto no momento de execução da query.

Para evitar a utilização de sintaxe proprietária do banco de dados, a Criteria API faz uso padronizado das palavras reservadas do banco de dados, como SELECT, FROM e WHERE. Veja no exemplo da Listagem 11 que não colocamos mais em formato de texto as palavras SELECT, FROM, etc.

Listagem 11. Exemplo de query utilizando Criteria API.

CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Cliente> c = cb.createQuery(Cliente.class); Root<Cliente> emp = c.from(Cliente.class); c.select(emp).where(cb.equal(emp.get(“nome”), “Mariana”));

Aparentemente a query ficou com mais linhas e mais complexa, mas vamos entender o que aconteceu e você perceberá que não é tão complexo assim. Para auxiliar nesta compreensão vamos traçar paralelos entre a versão JPQL e a versão baseada na Criteria API.

As palavras reservadas de JPQL como SELECT, FROM e WHERE correspondem respectivamente aos métodosselect(), from() e where().

O atributo class da entidade Cliente, utilizado no método from(), substitui o identificador da classe no JPQL.

Já o atributo nome, utilizado no método where(), foi inserido no método get() substituindo o trecho “c.nome”.

Em uma macro visão temos que a interface CriteriaBuilder é a principal porta de entrada na Criteria API, tendo como objetivo ser uma fábrica de objetos que ligados formarão uma consulta (select) ao banco de dados. Para obtermos uma instância desta interface basta chamar o método getCriteriaBuilder() da interface EntityManager.

A interface CriteriaBuilder também possui métodos para a construção de expressões condicionais, operadores e funções de JPQL na cláusula WHERE.

Outra interface importante na consulta com Criteria API é a interface CriteriaQuery, que pode ser obtida através da chamada do método createQuery() da interface CriteriaBuilder. A interface CriteriaQuery é responsável pelos métodos select(), from() e where().

Passo a passo para construção de queries

O primeiro passo na construção das queries é obter o objeto CriteriaBuilder, a partir de um EntityManager. O objeto CriteriaBuilder possui métodos para a criação do objeto CriteriaQuery, que define por sua vez a entidade principal a ser utilizada na query – em nosso exemplo utilizamos a classe Cliente.

CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Cliente> c = cb.createQuery(Cliente.class);

O próximo passo é estabelecer a raiz da consulta, ou seja, a classe principal da cláusula FROM. Para isso utilizamos o método from() a partir da interface CriteriaQuery. Isso equivale à declaração de uma variável de identificação, e formará a base para expressões que será utilizada como o caminho para o resto da consulta.

Root<Cliente> emp = c.from(Cliente.class);

A próxima etapa prevê a cláusula SELECT, passando a raiz da consulta no método select(). Com esta informação sabemos quais são os objetos que retornarão no resultado da execução da query.

c.select(emp)

O último passo é construir a cláusula WHERE, passando por uma expressão composta a partir de métodos de CriteriaBuilder que representam as expressões condicionais de JPQL. Quando estas expressões são necessárias, como acessar o atributo nome, o método get() do objeto raiz é usado para criar o acesso ao atributo que será utilizado na expressão onde combinaremos com o valor do parâmetro que queremos buscar.

where(cb.equal(emp.get(“nome”), “Mariana”));

O uso de Criteria API pode parecer menos vantajoso que escrever JPQL, porém deve ser levado em consideração que esta API traz vantagens para o código, como: padronização na sintaxe da query, e a possibilidade de validação da query em tempo de compilação (quando combinada com MetaModel API).

MetaModel API

Quando construímos queries com JPQL, normalmente utilizamos objetos String para inserir as informações nas queries. Se por um engano escrevermos o nome da classe ou qualquer outra informação errado, este erro só será descoberto em tempo de execução.

Por exemplo, se escrevermos em uma String a palavra “Clente” ao invés de Cliente para referenciar a entidade no meio da String, a exceção ocorrerá somente em tempo de execução, pois o compilador Java não é hábil suficiente para entender este engano.

Com o recurso MetaModel é possível criarmos queries fortemente tipadas, ou seja, queries que serão avaliadas em tempo de compilação.

Utilizando MetaModel

Para conseguirmos criar queries fortemente tipadas com JPA 2.0, devemos criar classes que irão auxiliar o compilador Java.

Sendo assim, para cada entidade a ser utilizada em queries fortemente tipadas, temos que criar uma nova classe que servirá como um modelo “macro”, para que o compilador obtenha informações dos atributos X tipo de dados X mapeamento. Veja o exemplo da entidade de Cliente na Listagem 12 e a nova classe MetaModel na Listagem 13.

Listagem 12. Classe da entidade de cliente.

public class Cliente implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Basic(optional = false) @Column(name = “id”, nullable = false) private Integer id; @Basic(optional = false) @Column(name = “nome”, nullable = false, length = 45) private String nome; @OneToMany(cascade = CascadeType.ALL, mappedBy = “pessoa”) private Collection<Endereco> enderecoCollection; }

Listagem 13. Classe MetaModel de cliente.

@StaticMetamodel(Cliente.class) public class Cliente_ { public static volatile SingularAttribute<Cliente, Integer> id; public static volatile SingularAttribute<Cliente, String> nome; public static volatile CollectionAttribute<Cliente, Endereco> project; }

Agora podemos escrever a query fortemente tipada, utilizando a classe auxiliar de MetaModel Cliente_ (Listagem 14).

Listagem 14. Exemplo de query utilizando Criteria API e MetaModel.

CriteriaBuilder cb = em.getCriteriaBuilder(); CriteriaQuery<Object> c = cb.createQuery(); Root<Cliente> emp = c.from(Cliente.class); c.select(emp).where(cb.equal(emp.get(Cliente_.nome), “Mariana”));

Neste exemplo, mapeamos os atributos da classe de entidade Cliente com seus respectivos tipos, a partir da classe auxiliar Cliente_. Alterando a consulta podemos perceber que o método get() do objeto raiz ao invés de utilizar uma String (“nome”) utiliza um atributo estático da classe auxiliar de MetaModel, e por este motivo o conteúdo da query poderá ser validado em tempo de compilação.

O processo de criação e manutenção das classes de MetaModel exige atenção, pois todas as alterações realizadas sobre as entidades devem ser refletidas nestas classes. Para auxiliar e facilitar este processo, é possível efetuá-lo de maneira automática utilizando a diretiva de compilação –processor, conforme o modelo:

javac -processor org.eclipse.persistence.internal.jpa.modelgen.CanonicalModelProcessor -proc:only -classpath …

Este recurso é muito interessante se pensarmos no beneficio de termos a construção de queries sem erros de sintaxe e com o uso de recursos de autocomplete das IDEs.

Outras melhorias

JPA 2.0 é uma das especificações do Java EE 6 que teve mais tempo para ser produzida. Em virtude disso, muitas melhorias foram realizadas. Citamos neste artigo algumas das principais alterações, mas certamente, há muito mais recursos nessa nova especificação. Por isso, para quem tiver mais interesse recomendamos fortemente a leitura da JSR 317.

Apenas para citar, houve melhorias nas APIs (por exemplo, adição de novos métodos nas classes EntityManager, EntityManagerFactory e Query), nos recursos de cache de segundo nível (Second Level Cache), nos recursos de locking (Optimistic Locking), em outros aspectos de mapeamento como a inclusão de recursos de validação de dados (Bean Validation), entre outras.

Uma melhoria que apesar de simples merece destaque é a padronização das propriedades de configuração do jdbc no arquivo persistence.xml. Usando JPA 2.0, as propriedades podem ser definidas de acordo com a Listagem 15.

Listagem 15. Trecho do persistence.xml com propriedades padronizadas.

<property name=“javax.persistence.jdbc.url” value=“jdbc:mysql://host:3306/<database>” /> <property name=“javax.persistence.jdbc.user” value=“<user>” /> <property name=“javax.persistence.jdbc.password” value=“<password>” /> <property name=“javax.persistence.jdbc.driver” value=“<driver name>” />
Conclusão

Conforme percebemos neste artigo, a API JPA 2.0 está totalmente madura, pois essa nova versão incorporou a maioria dos recursos utilizados nos frameworks de persistência mais comuns e, principalmente, resolveu as limitações que a versão 1.0 apresentava em relação aos sistemas legados.

Para quem quer colocar em prática, o mercado já oferece duas implementações para a JPA 2.0: EclipseLink, que é a RI (Reference Implementation), e Hibernate 3.5, que foi lançado recentemente. Ambas as implementações podem ser utilizadas tanto dentro de servidores Java EE quanto em ambientes de aplicação desktop. Vale a pena experimentar. Os exemplos deste artigo foram feitos usando IDE Eclipse e EclipseLink em ambiente desktop.

Para finalizar, vale ressaltar que os recursos adicionados à JPA 2.0 mantêm compatibilidade com a versão anterior. Assim, aplicações que já utilizam JPA 1.0 continuam funcionando sem necessidade de alteração ao migrar para a versão 2.0.

Links Úteis

Saiba mais sobre Java ;)

  • Guias Java:
    Encontre aqui os Guias de estudo que vão ajudar você a aprofundar seu conhecimento na linguagem Java. Desde o básico ao mais avançado. Escolha o seu!
  • Carreira Programador Java:
    Nesse Guia de Referência você encontrará o conteúdo que precisa para iniciar seus estudos sobre a tecnologia Java, base para o desenvolvimento de aplicações desktop, web e mobile/embarcadas.
  • Linguagem de Programação Java:
    Neste Guia de Referência você encontrará todo o conteúdo que precisa para começar a programar com a linguagem Java, a sua caixa de ferramentas base para criar aplicações com Java.

Artigos relacionados