Mapeamento Dinâmico com Hibernate
1. Introdução
Neste artigo, vamos explorar alguns recursos de mapeamento dinâmico do Hibernate com as anotações@Formula,@Where,@Filtere@Any.
Observe que, embora o Hibernate implemente a especificação JPA, as anotações descritas aqui estão disponíveis apenas no Hibernate e não são diretamente portáveis para outras implementações JPA.
2. Configuração do Projeto
Para demonstrar os recursos, precisaremos apenas da biblioteca hibernate-core e um banco de dados H2 de apoio:
org.hibernate
hibernate-core
5.2.12.Final
com.h2database
h2
1.4.194
Para a versão atual da bibliotecahibernate-core, vá paraMaven Central.
3. Colunas calculadas com@Formula
Suponha que desejemos calcular um valor de campo da entidade com base em algumas outras propriedades. Uma maneira de fazer isso seria definindo um campo somente leitura calculado em nossa entidade Java:
@Entity
public class Employee implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private long grossIncome;
private int taxInPercents;
public long getTaxJavaWay() {
return grossIncome * taxInPercents / 100;
}
}
A desvantagem óbvia é quewe’d have to do the recalculation each time we access this virtual field by the getter.
Seria muito mais fácil obter o valor já calculado do banco de dados. Isso pode ser feito com a anotação@Formula:
@Entity
public class Employee implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private long grossIncome;
private int taxInPercents;
@Formula("grossIncome * taxInPercents / 100")
private long tax;
}
Com@Formula, podemos usar subconsultas, chamar funções de banco de dados nativas e procedimentos armazenados e basicamente fazer qualquer coisa que não quebre a sintaxe de uma cláusula select do SQL para este campo.
O Hibernate é inteligente o suficiente para analisar o SQL que fornecemos e inserir aliases de tabela e campo corretos. A ressalva a ser observada é que, como o valor da anotação é SQL bruto, isso pode tornar nosso mapeamento dependente do banco de dados.
Além disso, lembre-se de quethe value is calculated when the entity is fetched from the database. Portanto, quando persistirmos ou atualizarmos a entidade, o valor não será recalculado até que a entidade seja removida do contexto e carregada novamente:
Employee employee = new Employee(10_000L, 25);
session.save(employee);
session.flush();
session.clear();
employee = session.get(Employee.class, employee.getId());
assertThat(employee.getTax()).isEqualTo(2_500L);
4. Filtrando entidades com@Where
Suponha que desejemos fornecer uma condição adicional à consulta sempre que solicitarmos alguma entidade.
Por exemplo, precisamos implementar a "exclusão reversível". Isso significa que a entidade nunca é excluída do banco de dados, mas apenas marcada como excluída com um campoboolean.
Teríamos que tomar muito cuidado com todas as consultas existentes e futuras no aplicativo. Teríamos que fornecer essa condição adicional para cada consulta. Felizmente, o Hibernate fornece uma maneira de fazer isso em um só lugar:
@Entity
@Where(clause = "deleted = false")
public class Employee implements Serializable {
// ...
}
A anotação@Where em um método contém uma cláusula SQL que será adicionada a qualquer consulta ou subconsulta a esta entidade:
employee.setDeleted(true);
session.flush();
session.clear();
employee = session.find(Employee.class, employee.getId());
assertThat(employee).isNull();
Como no caso da anotação@Formula,since we’re dealing with raw SQL, the @Where condition won’t be reevaluated until we flush the entity to the database and evict it from the context.
Até esse momento, a entidade ficará no contexto e estará acessível com consultas e pesquisas porid.
A anotação@Where também pode ser usada para um campo de coleção. Suponha que tenhamos uma lista de telefones excluídos:
@Entity
public class Phone implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private boolean deleted;
private String number;
}
Então, do ladoEmployee, poderíamos mapear uma coleção dephones deletáveis da seguinte maneira:
public class Employee implements Serializable {
// ...
@OneToMany
@JoinColumn(name = "employee_id")
@Where(clause = "deleted = false")
private Set phones = new HashSet<>(0);
}
A diferença é que a coleçãoEmployee.phones sempre seria filtrada, mas ainda poderíamos obter todos os telefones, incluindo os excluídos, por meio de consulta direta:
employee.getPhones().iterator().next().setDeleted(true);
session.flush();
session.clear();
employee = session.find(Employee.class, employee.getId());
assertThat(employee.getPhones()).hasSize(1);
List fullPhoneList
= session.createQuery("from Phone").getResultList();
assertThat(fullPhoneList).hasSize(2);
5. Filtragem Parametrizada com@Filter
O problema com a anotação@Where é que ela nos permite especificar apenas uma consulta estática sem parâmetros e não pode ser desativada ou ativada por demanda.
A anotação@Filter funciona da mesma maneira que@Where, mas também pode ser habilitada ou desabilitada no nível da sessão, e também parametrizada.
5.1. Definindo o@Filter
Para demonstrar como@Filter funciona, vamos primeiro adicionar a seguinte definição de filtro à entidadeEmployee:
@FilterDef(
name = "incomeLevelFilter",
parameters = @ParamDef(name = "incomeLimit", type = "int")
)
@Filter(
name = "incomeLevelFilter",
condition = "grossIncome > :incomeLimit"
)
public class Employee implements Serializable {
A anotação@FilterDef define o nome do filtro e um conjunto de seus parâmetros que participarão da consulta. O tipo do parâmetro é o nome de um dos tipos do Hibernate (Type,UserType ouCompositeUserType), no nosso caso, umint.
A anotaçãoThe @FilterDef pode ser colocada no tipo ou no nível do pacote. Observe que ele não especifica a própria condição do filtro (embora pudéssemos especificar o parâmetrodefaultCondition).
Isso significa que podemos definir o filtro (seu nome e conjunto de parâmetros) em um local e, em seguida, definir as condições para o filtro em vários outros locais de maneira diferente.
Isso pode ser feito com a anotação@Filter. No nosso caso, colocamos na mesma classe por simplicidade. A sintaxe da condição é um SQL bruto com nomes de parâmetros precedidos por dois pontos.
5.2. Acessando entidades filtradas
Outra diferença de@Filter de@Where é que@Filter não está habilitado por padrão. Temos que habilitá-lo no nível da sessão manualmente e fornecer os valores dos parâmetros para ele:
session.enableFilter("incomeLevelFilter")
.setParameter("incomeLimit", 11_000);
Agora, suponha que tenhamos os três funcionários a seguir no banco de dados:
session.save(new Employee(10_000, 25));
session.save(new Employee(12_000, 25));
session.save(new Employee(15_000, 25));
Depois, com o filtro ativado, como mostrado acima, apenas dois deles estarão visíveis consultando:
List employees = session.createQuery("from Employee")
.getResultList();
assertThat(employees).hasSize(2);
Observe que o filtro ativado e seus valores de parâmetro são aplicados somente dentro da sessão atual. Em uma nova sessão sem filtro habilitado, veremos todos os três funcionários:
session = HibernateUtil.getSessionFactory().openSession();
employees = session.createQuery("from Employee").getResultList();
assertThat(employees).hasSize(3);
Além disso, ao buscar diretamente a entidade por ID, o filtro não é aplicado:
Employee employee = session.get(Employee.class, 1);
assertThat(employee.getGrossIncome()).isEqualTo(10_000);
5.3. @Filter e cache de segundo nível
Se temos um aplicativo de alta carga, então definitivamente queremos habilitar o cache de segundo nível do Hibernate, o que pode ser um grande benefício de desempenho. Devemos ter em mente quethe @Filter annotation does not play nicely with caching.
The second-level cache only keeps full unfiltered collections. Se não fosse o caso, poderíamos ler uma coleção em uma sessão com o filtro ativado e, em seguida, obter a mesma coleção filtrada em cache em outra sessão, mesmo com o filtro desativado.
É por isso que a anotação@Filter basicamente desabilita o armazenamento em cache para a entidade.
6. Mapeando Qualquer Referência de Entidade com@Any
Às vezes, queremos mapear uma referência para qualquer um dos vários tipos de entidade, mesmo se eles não forem baseados em um único@MappedSuperclass. Eles podem até ser mapeados para diferentes tabelas não relacionadas. Podemos fazer isso com a anotação@Any.
Em nosso exemplo,we’ll need to attach some description to every entity in our persistence unit, a saber,EmployeeePhone. Não seria razoável herdar todas as entidades de uma única superclasse abstrata apenas para fazer isso.
6.1. Relação de mapeamento com@Any
Veja como podemos definir uma referência a qualquer entidade que implementeSerializable (ou seja, a qualquer entidade):
@Entity
public class EntityDescription implements Serializable {
private String description;
@Any(
metaDef = "EntityDescriptionMetaDef",
metaColumn = @Column(name = "entity_type"))
@JoinColumn(name = "entity_id")
private Serializable entity;
}
A propriedademetaDef é o nome da definição emetaColumn é o nome da coluna que será usada para distinguir o tipo de entidade (não diferente da coluna discriminadora no mapeamento de hierarquia de tabela única).
Também especificamos a coluna que fará referência aid da entidade. É importante notar quethis column will not be a foreign key porque ele pode fazer referência a qualquer tabela que quisermos.
Geralmente, a colunaentity_id também não pode ser única porque tabelas diferentes podem ter identificadores repetidos.
O parentity_type /entity_id, no entanto, deve ser único, pois descreve de forma exclusiva a entidade à qual estamos nos referindo.
6.2. Definindo o mapeamento de@Any com@AnyMetaDef
No momento, o Hibernate não sabe como distinguir os diferentes tipos de entidade, porque não especificamos o que a colunaentity_type pode conter.
Para fazer este trabalho, precisamos adicionar a meta-definição do mapeamento com a anotação@AnyMetaDef. O melhor lugar para colocá-lo seria o nível do pacote, para podermos reutilizá-lo em outros mapeamentos.
Veja como ficaria o arquivopackage-info.java com a anotação@AnyMetaDef:
@AnyMetaDef(
name = "EntityDescriptionMetaDef",
metaType = "string",
idType = "int",
metaValues = {
@MetaValue(value = "Employee", targetEntity = Employee.class),
@MetaValue(value = "Phone", targetEntity = Phone.class)
}
)
package com.example.hibernate.pojo;
Aqui nós especificamos o tipo da colunaentity_type (string), o tipo da colunaentity_id (int), os valores aceitáveis ementity_typecoluna s (“Employee”e“Phone”) e os tipos de entidade correspondentes.
Agora, suponha que tenhamos um funcionário com dois telefones descritos assim:
Employee employee = new Employee();
Phone phone1 = new Phone("555-45-67");
Phone phone2 = new Phone("555-89-01");
employee.getPhones().add(phone1);
employee.getPhones().add(phone2);
Agora, podemos adicionar metadados descritivos às três entidades, mesmo que elas tenham tipos diferentes não relacionados:
EntityDescription employeeDescription = new EntityDescription(
"Send to conference next year", employee);
EntityDescription phone1Description = new EntityDescription(
"Home phone (do not call after 10PM)", phone1);
EntityDescription phone2Description = new EntityDescription(
"Work phone", phone1);
7. Conclusão
Neste artigo, exploramos algumas das anotações do Hibernate que permitem o ajuste fino do mapeamento de entidades usando SQL bruto.
O código-fonte do artigo está disponívelover on GitHub.