Hibernate 动态映射
1. 简介
在本文中,我们将使用*@Formula*、@Where、@Filter和*@Any*注解探索 Hibernate 的一些动态映射功能。
请注意,尽管 Hibernate 实现了 JPA 规范,但此处描述的注解仅在 Hibernate 中可用,并且不能直接移植到其他 JPA 实现。
2. 项目设置
为了演示这些特性,我们只需要 hibernate-core 库和一个支持 H2 数据库:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>5.4.12.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.194</version>
</dependency>
对于当前版本的hibernate-core库,请访问Maven Central 。
3. 使用*@Formula*计算的列
假设我们要根据其他一些属性计算一个实体字段值。一种方法是在我们的 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;
}
}
明显的缺点是每次我们通过 getter 访问这个虚拟字段时都必须重新计算。
从数据库中获取已经计算的值会容易得多。这可以通过*@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;
}
使用@Formula,我们可以使用子查询、调用本地数据库函数和存储过程,并且基本上可以做任何不破坏该字段的 SQL 选择子句语法的事情。**
Hibernate 足够聪明,可以解析我们提供的 SQL 并插入正确的表和字段别名。需要注意的是,由于注解的值是原始 SQL,它可能使我们的映射依赖于数据库。
另外,请记住,该值是在从数据库中获取实体时计算的。因此,当我们持久化或更新实体时,在将实体从上下文中逐出并再次加载之前,不会重新计算值:
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. 使用*@Where*过滤实体
假设我们希望在请求某个实体时为查询提供附加条件。
例如,我们需要实现“软删除”。这意味着实体永远不会从数据库中删除,而只是用布尔字段标记为已删除。
我们必须非常小心地处理应用程序中所有现有和未来的查询。我们必须为每个查询提供这个附加条件。幸运的是,Hibernate 提供了一种在一个地方执行此操作的方法:
@Entity
@Where(clause = "deleted = false")
public class Employee implements Serializable {
// ...
}
方法上的*@Where*注解包含一个 SQL 子句,该子句将添加到该实体的任何查询或子查询中:
employee.setDeleted(true);
session.flush();
session.clear();
employee = session.find(Employee.class, employee.getId());
assertThat(employee).isNull();
与*@Formula注解的情况一样,*由于我们正在处理原始 SQL,因此在我们将实体刷新到数据库并将其从 context 中逐出之前,不会重新评估@Where条件。**
在那之前,实体将保留在上下文中,并且可以通过id查询和查找来访问。
@Where注解也可以用于集合字段。假设我们有一个可删除电话列表:
@Entity
public class Phone implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private boolean deleted;
private String number;
}
然后,从Employee端,我们可以映射一组可删除的phones,如下所示:
public class Employee implements Serializable {
// ...
@OneToMany
@JoinColumn(name = "employee_id")
@Where(clause = "deleted = false")
private Set<Phone> phones = new HashSet<>(0);
}
不同之处在于Employee.phones集合总是会被过滤,但我们仍然可以通过直接查询获得所有电话,包括已删除的电话:
employee.getPhones().iterator().next().setDeleted(true);
session.flush();
session.clear();
employee = session.find(Employee.class, employee.getId());
assertThat(employee.getPhones()).hasSize(1);
List<Phone> fullPhoneList
= session.createQuery("from Phone").getResultList();
assertThat(fullPhoneList).hasSize(2);
5. 使用*@Filter* 进行参数化过滤
@Where注解的问题在于它允许我们只指定一个不带参数的静态查询,并且不能按需禁用或启用。
@Filter注解的工作方式与@Where相同,但它也可以在会话级别启用或禁用,也可以参数化。**
5.1. 定义*@Filter*
为了演示*@Filter的工作原理,我们首先将以下过滤器定义添加到Employee*实体:
@FilterDef(
name = "incomeLevelFilter",
parameters = @ParamDef(name = "incomeLimit", type = "int")
)
@Filter(
name = "incomeLevelFilter",
condition = "grossIncome > :incomeLimit"
)
public class Employee implements Serializable {
@FilterDef注解定义过滤器名称和一组将参与查询的参数。参数的类型是 Hibernate 类型之一(Type 、UserType 或CompositeUserType )的名称,在我们的例子中是int。
@FilterDef注解可以放在类型或包级别。请注意,它并没有指定过滤条件本身(尽管我们可以指定defaultCondition参数)。
这意味着我们可以在一个地方定义过滤器(它的名称和参数集),然后在多个其他地方以不同的方式定义过滤器的条件。
这可以通过*@Filter*注解来完成。在我们的例子中,为了简单起见,我们把它放在同一个类中。条件的语法是一个原始 SQL,参数名称以冒号开头。
5.2. 访问过滤的实体
@Filter与*@Where的另一个区别是@Filter*默认不启用。我们必须在会话级别手动启用它,并为其提供参数值:
session.enableFilter("incomeLevelFilter")
.setParameter("incomeLimit", 11_000);
现在假设我们在数据库中有以下三个员工:
session.save(new Employee(10_000, 25));
session.save(new Employee(12_000, 25));
session.save(new Employee(15_000, 25));
然后在启用过滤器的情况下,如上所示,通过查询只能看到其中两个:
List<Employee> employees = session.createQuery("from Employee")
.getResultList();
assertThat(employees).hasSize(2);
请注意,启用的过滤器及其参数值都仅在当前会话中应用。在未启用过滤器的新会话中,我们将看到所有三个员工:
session = HibernateUtil.getSessionFactory().openSession();
employees = session.createQuery("from Employee").getResultList();
assertThat(employees).hasSize(3);
此外,当直接通过 id 获取实体时,不应用过滤器:
Employee employee = session.get(Employee.class, 1);
assertThat(employee.getGrossIncome()).isEqualTo(10_000);
5.3. @Filter和二级缓存
如果我们有一个高负载的应用程序,那么我们肯定希望启用 Hibernate 二级缓存,这可以带来巨大的性能优势。我们应该记住,@Filter注解不能很好地与缓存一起使用。
二级缓存只保留完整的未过滤集合。如果不是这种情况,那么我们可以在启用过滤器的一个会话中读取一个集合,然后在另一个会话中获取相同的缓存过滤集合,即使禁用过滤器也是如此。
这就是@Filter*注解基本上禁用实体缓存的原因。*
6. 使用*@Any*映射任何实体引用
有时我们希望将引用映射到多个实体类型中的任何一个,即使它们不是基于单个*@MappedSuperclass*。它们甚至可以映射到不同的不相关表。我们可以使用*@Any*注解来实现这一点。
在我们的示例中,我们需要为持久化单元中的每个实体(即Employee和Phone )附加一些描述。仅仅为了做到这一点而从单个抽象超类继承所有实体是不合理的。
6.1. 与*@Any*映射关系
以下是我们如何定义对实现Serializable的任何实体的引用(即,对任何实体的引用):
@Entity
public class EntityDescription implements Serializable {
private String description;
@Any(
metaDef = "EntityDescriptionMetaDef",
metaColumn = @Column(name = "entity_type"))
@JoinColumn(name = "entity_id")
private Serializable entity;
}
metaDef属性是定义的名称,而metaColumn是将用于区分实体类型的列的名称(与单表层次映射中的鉴别器列不同)。
我们还指定将引用实体id的列。值得注意的是,该列不会是外键,因为它可以引用我们想要的任何表。
entity_id列通常也不能是唯一的,因为不同的表可能具有重复的标识符。
然而, entity_type/entity_id 对应该是唯一的,因为它唯一地描述了我们所指的实体。
6.2. 使用*@AnyMetaDef定义@Any*映射
目前,Hibernate 不知道如何区分不同的实体类型,因为我们没有指定entity_type列可以包含什么。
为了完成这项工作,我们需要使用*@AnyMetaDef*注解添加映射的元定义。放置它的最佳位置是包级别,因此我们可以在其他映射中重用它。
下面是带有*@AnyMetaDef注解的package-info.java*文件的样子:
@AnyMetaDef(
name = "EntityDescriptionMetaDef",
metaType = "string",
idType = "int",
metaValues = {
@MetaValue(value = "Employee", targetEntity = Employee.class),
@MetaValue(value = "Phone", targetEntity = Phone.class)
}
)
package com.blogdemo.hibernate.pojo;
这里我们指定了entity_type列的类型(string)、entity_id列的类型(int)、entity_type列中的可接受值(“Employee”和“Phone”)以及相应的实体类型。
现在,假设我们有一个员工有两部电话,描述如下:
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);
现在我们可以为所有三个实体添加描述性元数据,即使它们具有不同的不相关类型:
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);