1. 概述

本文将探讨 Hibernate 提供的几种动态映射能力,重点分析 @Formula@Where@Filter@Any 注解的使用场景和实现原理。

⚠️ 需要注意:虽然 Hibernate 实现了 JPA 规范,但本文讨论的注解是 Hibernate 特有的,无法直接移植到其他 JPA 实现中。

2. 项目配置

为演示这些特性,我们只需引入 hibernate-core 和 H2 数据库依赖:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.1.7.Final</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>2.1.214</version>
</dependency>

最新版本可查阅 Maven 中央仓库

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;

}

核心优势

  • 支持子查询、原生数据库函数和存储过程
  • 可在 SQL SELECT 子句中使用任何合法语法
  • Hibernate 会自动解析 SQL 并插入正确的表/字段别名

⚠️ 注意事项

  1. 由于使用原生 SQL,可能导致映射与特定数据库绑定

  2. 计算值仅在实体从数据库加载时执行:

    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 过滤实体

当需要为实体查询添加全局条件时(如实现软删除),@Where 是理想选择:

@Entity
@Where(clause = "deleted = false")
public class Employee implements Serializable {
    // ...
}

该注解会自动将 SQL 条件添加到所有涉及该实体的查询中:

employee.setDeleted(true);

session.flush();
session.clear();

employee = session.find(Employee.class, employee.getId());
assertThat(employee).isNull();

⚠️ 关键特性

  1. 条件在实体刷新到数据库并从上下文驱逐后才重新评估
  2. 期间实体仍存在于持久化上下文中,可通过 ID 查询访问

集合过滤示例: 假设有可删除的 Phone 实体:

@Entity
public class Phone implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    private boolean deleted;
    private String number;
}

Employee 中映射过滤后的集合:

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 支持动态参数和会话级控制:

5.1 定义过滤器

@FilterDef(
    name = "incomeLevelFilter", 
    parameters = @ParamDef(name = "incomeLimit", type = Integer.class)
)
@Filter(
    name = "incomeLevelFilter", 
    condition = "grossIncome > :incomeLimit"
)
public class Employee implements Serializable {

核心概念

  • @FilterDef 定义过滤器名称和参数类型
  • @Filter 指定具体 SQL 条件(参数以 : 前缀)
  • 可在类或包级别定义,实现条件复用

5.2 使用过滤器

过滤器默认未启用,需手动激活并设置参数:

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

⚠️ 作用域限制

  1. 过滤器仅在当前会话有效
  2. 新会话需重新启用:
    session = HibernateUtil.getSessionFactory().openSession();
    employees = session.createQuery("from Employee").getResultList();
    assertThat(employees).hasSize(3);
    
  3. 通过 ID 直接获取实体时不应用过滤器:
    Employee employee = session.get(Employee.class, 1);
    assertThat(employee.getGrossIncome()).isEqualTo(10_000);
    

5.3 与二级缓存的冲突

重要限制@Filter 与二级缓存不兼容

  • 二级缓存仅存储完整未过滤的集合
  • 若允许缓存过滤结果,会导致不同会话间数据不一致
  • 使用 @Filter 会自动禁用实体缓存

6. 使用 @Any 映射多态关联

当需要引用多个不相关实体类型时(即使它们没有共同父类),@Any 是解决方案:

6.1 定义多态关联

@Entity
public class EntityDescription implements Serializable {

    private String description;

    @Any
    @AnyDiscriminator(DiscriminatorType.STRING)
    @AnyDiscriminatorValue(discriminator = "S", entity = Employee.class)
    @AnyDiscriminatorValue(discriminator = "I", entity = Phone.class)
    @AnyKeyJavaClass(Integer.class)
    @Column(name = "entity_type")
    @JoinColumn(name = "entity_id")
    private Serializable entity;
}

关键配置

  • @AnyDiscriminator 指定类型区分列
  • @AnyDiscriminatorValue 定义类型标识符与实体类的映射
  • @AnyKeyJavaClass 声明 ID 字段的 Java 类型
  • entity_typeentity_id 列共同组成唯一引用

⚠️ 数据库约束

  • entity_id 不是外键(可引用任意表)
  • 单独的 entity_id 不要求唯一
  • (entity_type, entity_id) 组合必须唯一

6.2 使用示例

可为不同类型实体添加描述:

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. 总结

本文深入探讨了 Hibernate 的动态映射注解:

  • @Formula:实现数据库级计算列
  • @Where:添加全局静态过滤条件
  • @Filter:支持参数化和会话级控制的动态过滤
  • @Any:构建多态关联引用

这些特性通过原生 SQL 提供了强大的映射灵活性,但需注意数据库依赖性和缓存兼容性问题。

完整示例代码可在 GitHub 获取。