Dynamic Mapping with Hibernate – 使用Hibernate的动态映射

最后修改: 2017年 10月 30日

1. Introduction

1.介绍

In this article, we’ll explore some dynamic mapping capabilities of Hibernate with the @Formula, @Where, @Filter and @Any annotations.

在这篇文章中,我们将利用@Formula@Where@Filter@Any注解来探索Hibernate的一些动态映射功能。

Note that although Hibernate implements the JPA specification, annotations described here are available only in Hibernate and are not directly portable to other JPA implementations.

注意,尽管Hibernate实现了JPA规范,但这里描述的注解只在Hibernate中可用,不能直接移植到其他JPA实现中。

2. Project Setup

2.项目设置

To demonstrate the features, we’ll only need the hibernate-core library and a backing H2 database:

为了演示这些功能,我们只需要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>

For the current version of the hibernate-core library, head over to Maven Central.

关于当前版本的hibernate-core库,请访问Maven Central

3. Calculated Columns With @Formula

3.用@Formula计算的列

Suppose we want to calculate an entity field value based on some other properties. One way to do it would be by defining a calculated read-only field in our Java entity:

假设我们想根据其他一些属性来计算一个实体字段的值。一种方法是在我们的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;
    }

}

The obvious drawback is that we’d have to do the recalculation each time we access this virtual field by the getter.

明显的缺点是,我们每次通过getter访问这个虚拟字段时,都必须进行重新计算

It would be much easier to get the already calculated value from the database. This can be done with the @Formula annotation:

如果从数据库中获取已经计算好的值,那就容易多了。这可以用@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;

}

With @Formula, we can use subqueries, call native database functions and stored procedures and basically do anything that does not break the syntax of an SQL select clause for this field.

通过@Formula,我们可以使用子查询,调用本地数据库函数和存储过程,基本上可以做任何不会破坏这个字段的SQL选择子句的语法。

Hibernate is smart enough to parse the SQL we provided and insert correct table and field aliases. The caveat to be aware of is that since the value of the annotation is raw SQL, it may make our mapping database-dependent.

Hibernate足够聪明,可以解析我们提供的SQL并插入正确的表和字段别名。需要注意的是,由于注释的值是原始SQL,它可能会使我们的映射依赖于数据库。

Also, keep in mind that the value is calculated when the entity is fetched from the database. Hence, when we persist or update the entity, the value would not be recalculated until the entity is evicted from the context and loaded again:

另外,请记住,当实体从数据库中获取时,该值是计算出来的。因此,当我们持久化或更新实体时,该值将不会被重新计算,直到实体从上下文中被驱逐并再次加载。

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. Filtering Entities With @Where

4.用@Where过滤实体

Suppose we want to provide an additional condition to the query whenever we request some entity.

假设我们想在每次请求某个实体时为查询提供一个附加条件。

For instance, we need to implement “soft delete”. This means that the entity is never deleted from the database, but only marked as deleted with a boolean field.

例如,我们需要实现 “软删除”。这意味着实体永远不会从数据库中删除,而只是用一个boolean字段标记为删除。

We’d have to take great care with all existing and future queries in the application. We’d have to provide this additional condition to every query. Fortunately, Hibernate provides a way to do this in one place:

我们必须对应用程序中的所有现有和未来的查询采取非常谨慎的态度。我们必须为每个查询提供这个额外的条件。幸运的是,Hibernate提供了一种方法,可以在一个地方做到这一点。

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

    // ...
}

The @Where annotation on a method contains an SQL clause that will be added to any query or subquery to this entity:

方法上的@Where注解包含一个SQL子句,该子句将被添加到该实体的任何查询或子查询中。

employee.setDeleted(true);

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

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

As in the case of @Formula annotation, 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.

@Formula注解的情况一样,由于我们处理的是原始SQL,@Where条件不会被重新评估,直到我们将实体刷入数据库并将其从上下文中逐出

Until that time, the entity will stay in the context and will be accessible with queries and lookups by id.

在此之前,该实体将留在上下文中,并且可以通过id进行查询和查找。

The @Where annotation can also be used for a collection field. Suppose we have a list of deletable phones:

@Where注解也可以用于集合字段。假设我们有一个可删除电话的列表。

@Entity
public class Phone implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private boolean deleted;

    private String number;

}

Then, from the Employee side, we could map a collection of deletable phones as follows:

然后,从雇员方面,我们可以将一个可删除的电话集合映射成如下。

public class Employee implements Serializable {
    
    // ...

    @OneToMany
    @JoinColumn(name = "employee_id")
    @Where(clause = "deleted = false")
    private Set<Phone> phones = new HashSet<>(0);

}

The difference is that the Employee.phones collection would always be filtered, but we still could get all phones, including deleted ones, via direct query:

不同的是,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. Parameterized Filtering With @Filter

5.使用@Filter的参数化过滤。

The problem with @Where annotation is that it allows us to only specify a static query without parameters, and it can’t be disabled or enabled by demand.

@Where注解的问题是,它只允许我们指定一个没有参数的静态查询,而且它不能根据需求禁用或启用。

The @Filter annotation works the same way as @Where, but it also can be enabled or disabled on session level, and also parameterized.

@Filter注解的工作方式与@Where相同,但它也可以在会话级别上启用或禁用,并且也可以参数化。

5.1. Defining the @Filter

5.1.定义@Filter

To demonstrate how @Filter works, let’s first add the following filter definition to the Employee entity:

为了演示@Filter是如何工作的,让我们首先向Employee实体添加以下过滤器定义。

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

The @FilterDef annotation defines the filter name and a set of its parameters that will participate in the query. The type of the parameter is the name of one of the Hibernate types (Type, UserType or CompositeUserType), in our case, an int.

@FilterDef注解定义了过滤器的名称和一组将参与查询的其参数。参数的类型是Hibernate类型之一的名称(类型UserTypeCompositeUserType),在我们的例子中是一个int

The @FilterDef annotation may be placed either on the type or on package level. Note that it does not specify the filter condition itself (although we could specify the defaultCondition parameter).

@FilterDef 注解可以放在类型上,也可以放在包层上。请注意,它并不指定过滤条件本身(尽管我们可以指定defaultCondition参数)。

This means that we can define the filter (its name and set of parameters) in one place and then define the conditions for the filter in multiple other places differently.

这意味着我们可以在一个地方定义过滤器(其名称和一组参数),然后在其他多个地方以不同的方式定义过滤器的条件。

This can be done with the @Filter annotation. In our case, we put it in the same class for simplicity. The syntax of the condition is a raw SQL with parameter names preceded by colons.

这可以用@Filter注解来完成。在我们的案例中,为了简单起见,我们把它放在同一个类中。条件的语法是一个原始的SQL,参数名前面有冒号。

5.2. Accessing Filtered Entities

5.2.访问经过过滤的实体

Another difference of @Filter from @Where is that @Filter is not enabled by default. We have to enable it on the session level manually, and provide the parameter values for it:

@Filter@Where的另一个区别是,@Filter默认不启用。我们必须在会话级别手动启用它,并为它提供参数值。

session.enableFilter("incomeLevelFilter")
  .setParameter("incomeLimit", 11_000);

Now suppose we have the following three employees in the database:

现在假设我们在数据库中有以下三个雇员。

session.save(new Employee(10_000, 25));
session.save(new Employee(12_000, 25));
session.save(new Employee(15_000, 25));

Then with the filter enabled, as shown above, only two of them will be visible by querying:

然后在启用过滤器的情况下,如上图所示,通过查询只能看到其中的两个。

List<Employee> employees = session.createQuery("from Employee")
  .getResultList();
assertThat(employees).hasSize(2);

Note that both the enabled filter and its parameter values are applied only inside the current session. In a new session without filter enabled, we’ll see all three employees:

注意,启用的过滤器和它的参数值都只在当前会话中应用。在一个没有启用过滤器的新会话中,我们会看到所有三个雇员。

session = HibernateUtil.getSessionFactory().openSession();
employees = session.createQuery("from Employee").getResultList();
assertThat(employees).hasSize(3);

Also, when directly fetching the entity by id, the filter is not applied:

另外,当直接通过id获取实体时,过滤器没有被应用。

Employee employee = session.get(Employee.class, 1);
assertThat(employee.getGrossIncome()).isEqualTo(10_000);

5.3. @Filter and Second-Level Caching

5.3.@Filter和二级缓存

If we have a high-load application, then we’d definitely want to enable Hibernate second-level cache, which can be a huge performance benefit. We should keep in mind that the @Filter annotation does not play nicely with caching.

如果我们有一个高负载的应用程序,那么我们肯定想启用Hibernate二级缓存,这可以带来巨大的性能优势。我们应该记住,@Filter注解并不能很好地与缓存配合。

The second-level cache only keeps full unfiltered collections. If it wasn’t the case, then we could read a collection in one session with filter enabled, and then get the same cached filtered collection in another session even with filter disabled.

二级缓存只保留完整的未经过滤的集合。如果不是这样的话,那么我们可以在一个会话中启用过滤器读取一个集合,然后在另一个会话中即使禁用过滤器也能得到同样的缓存过滤的集合。

This is why the @Filter annotation basically disables caching for the entity.

这就是为什么@Filter注解基本上禁用了实体的缓存。

6. Mapping Any Entity Reference With @Any

6.用@Any映射任何实体引用

Sometimes we want to map a reference to any of multiple entity types, even if they are not based on a single @MappedSuperclass. They could even be mapped to different unrelated tables. We can achieve this with the @Any annotation.

有时我们想把一个引用映射到多个实体类型中的任何一个,即使它们不是基于一个@MappedSuperclass。它们甚至可以被映射到不同的不相关的表。我们可以通过@Any注解来实现这一点。

In our example, we’ll need to attach some description to every entity in our persistence unit, namely, Employee and Phone. It’d be unreasonable to inherit all entities from a single abstract superclass just to do this.

在我们的例子中,我们需要为持久化单元中的每个实体附加一些描述,即EmployeePhone。如果只是为了完成这个任务而从一个抽象的超类中继承所有实体,那是不合理的。

6.1. Mapping Relation With @Any

6.1.与@Any的映射关系。

Here’s how we can define a reference to any entity that implements Serializable (i.e., to any entity at all):

下面是我们如何定义对任何实现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;

}

The metaDef property is the name of the definition, and metaColumn is the name of the column that will be used to distinguish the entity type (not unlike the discriminator column in the single table hierarchy mapping).

metaDef属性是定义的名称,而metaColumn是将用于区分实体类型的列的名称(与单表层次映射中的判别列不一样)。

We also specify the column that will reference the id of the entity. It’s worth noting that this column will not be a foreign key because it can reference any table that we want.

我们还指定了将引用实体的id的列。值得注意的是,这个列不会是外键,因为它可以引用我们想要的任何表。

The entity_id column also can’t generally be unique because different tables could have repeated identifiers.

entity_id列一般也不能是唯一的,因为不同的表可能有重复的标识符。

The entity_type/entity_id pair, however, should be unique, as it uniquely describes the entity that we’re referring to.

entity_type/entity_id对应该是唯一的,因为它唯一地描述了我们所指的实体。

6.2. Defining the @Any Mapping With @AnyMetaDef

6.2.用@Any映射来定义@AnyMetaDef

Right now, Hibernate does not know how to distinguish different entity types, because we did not specify what the entity_type column could contain.

现在,Hibernate不知道如何区分不同的实体类型,因为我们没有指定entity_type列可以包含什么。

To make this work, we need to add the meta-definition of the mapping with the @AnyMetaDef annotation. The best place to put it would be the package level, so we could reuse it in other mappings.

为了使其发挥作用,我们需要用@AnyMetaDef注解添加映射的元定义。放置它的最佳位置是包级,所以我们可以在其他映射中重用它。

Here’s how the package-info.java file with the @AnyMetaDef annotation would look like:

下面是带有@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.baeldung.hibernate.pojo;

Here we’ve specified the type of the entity_type column (string), the type of the entity_id column (int), the acceptable values in the entity_type column (“Employee” and “Phone”) and the corresponding entity types.

这里我们指定了entity_type列的类型(string),entity_id列的类型(int),entity_type列的可接受值(“Employee”“Phone”)以及相应的实体类型。

Now, suppose we have an employee with two phones described like this:

现在,假设我们有一个员工有两部电话,描述如下。

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

Now we could add descriptive metadata to all three entities, even though they have different unrelated types:

现在,我们可以为这三个实体添加描述性元数据,尽管它们有不同的非相关类型。

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. Conclusion

7.结论

In this article, we’ve explored some of Hibernate’s annotations that allow fine-tuning entity mapping using raw SQL.

在这篇文章中,我们探讨了Hibernate的一些注释,这些注释允许使用原始SQL对实体映射进行微调。

The source code for the article is available over on GitHub.

文章的源代码可在GitHub上获得over