Contents

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 类型之一(TypeUserTypeCompositeUserType )的名称,在我们的例子中是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*注解来实现这一点。

在我们的示例中,我们需要为持久化单元中的每个实体(即EmployeePhone )附加一些描述。仅仅为了做到这一点而从单个抽象超类继承所有实体是不合理的。

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);