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 并插入正确的表/字段别名
⚠️ 注意事项:
由于使用原生 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
过滤实体
当需要为实体查询添加全局条件时(如实现软删除),@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();
⚠️ 关键特性:
- 条件在实体刷新到数据库并从上下文驱逐后才重新评估
- 期间实体仍存在于持久化上下文中,可通过 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);
⚠️ 作用域限制:
- 过滤器仅在当前会话有效
- 新会话需重新启用:
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
与二级缓存不兼容
- 二级缓存仅存储完整未过滤的集合
- 若允许缓存过滤结果,会导致不同会话间数据不一致
- 使用
@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_type
和entity_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 获取。