Hibernate를 사용한 동적 매핑

1. 소개

이 기사에서는 @Formula , @Where , @Filter@Any 주석을 사용하여 Hibernate의 몇 가지 동적 매핑 기능을 살펴 보겠습니다 .

Hibernate가 JPA 사양을 구현하더라도 여기에 설명 된 주석은 Hibernate에서만 사용할 수 있으며 다른 JPA 구현으로 직접 이식 할 수 없습니다.

2. 프로젝트 설정

기능을 시연하려면 hibernate-core 라이브러리와 지원 H2 데이터베이스 만 있으면됩니다.

 org.hibernate hibernate-core 5.4.12.Final   com.h2database h2 1.4.194 

현재 버전의 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 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로 엔티티 필터링

어떤 엔터티를 요청할 때마다 쿼리에 추가 조건을 제공한다고 가정합니다.

예를 들어 "소프트 삭제"를 구현해야합니다. 이는 엔티티가 데이터베이스에서 삭제되지 않고 부울 필드를 사용하여 삭제 된 것으로 표시된다는 것을 의미 합니다.

우리는 응용 프로그램의 모든 기존 및 향후 쿼리에 대해 세심한주의를 기울여야합니다. 모든 쿼리에이 추가 조건을 제공해야합니다. 다행히 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을 다루기 때문에 엔티티를 데이터베이스로 플러시하고 컨텍스트에서 제거 할 때까지 @Where 조건이 재평가되지 않습니다 .

그 때까지 엔티티는 컨텍스트에 머물며 id 로 조회 및 조회를 통해 액세스 할 수 있습니다 .

@Where의 주석은 또한 컬렉션 필드에 사용할 수 있습니다. 삭제 가능한 전화 목록이 있다고 가정합니다.

@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 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 fullPhoneList = session.createQuery("from Phone").getResultList(); assertThat(fullPhoneList).hasSize(2);

5. @Filter를 사용한 매개 변수화 된 필터링

@Where 어노테이션 의 문제점 은 매개 변수없이 정적 쿼리 만 지정할 수 있으며 요청에 따라 비활성화하거나 활성화 할 수 없다는 것입니다.

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

5.1. Defining the @Filter

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

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

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

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.

5.2. Accessing Filtered Entities

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:

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 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:

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

5.3. @Filter and Second-Level Caching

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.

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.

6. Mapping Any Entity Reference With @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.

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.

6.1. Mapping Relation With @Any

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

@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).

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.

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

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

6.2. Defining the @Any Mapping With @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.

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.

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

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

여기에서는 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);

7. 결론

이 기사에서 우리는 원시 SQL을 사용하여 엔티티 매핑을 미세 조정할 수있는 Hibernate의 주석 중 일부를 탐색했습니다.

기사의 소스 코드는 GitHub에서 사용할 수 있습니다.