Bản đồ động với Hibernate

1. Giới thiệu

Trong bài viết này, chúng ta sẽ khám phá một số khả năng lập bản đồ năng động của Hibernate với @Formula , @Where , @Filter@Any chú thích.

Lưu ý rằng mặc dù Hibernate triển khai đặc tả JPA, các chú thích được mô tả ở đây chỉ có sẵn trong Hibernate và không trực tiếp di động sang các triển khai JPA khác.

2. Thiết lập dự án

Để chứng minh các tính năng, chúng tôi sẽ chỉ cần thư viện lõi ngủ đông và cơ sở dữ liệu H2 hỗ trợ:

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

Đối với phiên bản hiện tại của thư viện lõi ngủ đông , hãy đến Maven Central.

3. Các cột được tính toán với @Formula

Giả sử chúng ta muốn tính toán một giá trị trường thực thể dựa trên một số thuộc tính khác. Một cách để làm điều đó sẽ là xác định trường chỉ đọc được tính toán trong thực thể Java của chúng tôi:

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

Hạn chế rõ ràng là chúng tôi sẽ phải tính toán lại mỗi khi chúng tôi truy cập vào trường ảo này bằng getter .

Sẽ dễ dàng hơn nhiều để lấy giá trị đã được tính toán từ cơ sở dữ liệu. Điều này có thể được thực hiện với chú thích @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; }

Với @Formula , chúng ta có thể sử dụng truy vấn con, gọi các hàm cơ sở dữ liệu gốc và các thủ tục được lưu trữ và về cơ bản làm bất cứ điều gì không phá vỡ cú pháp của mệnh đề SQL select cho trường này.

Hibernate đủ thông minh để phân tích cú pháp SQL mà chúng tôi đã cung cấp và chèn các bí danh bảng và trường chính xác. Lưu ý cần biết là vì giá trị của chú thích là SQL thô, nó có thể làm cho cơ sở dữ liệu ánh xạ của chúng ta phụ thuộc vào.

Ngoài ra, hãy nhớ rằng giá trị được tính khi thực thể được tìm nạp từ cơ sở dữ liệu . Do đó, khi chúng tôi duy trì hoặc cập nhật thực thể, giá trị sẽ không được tính toán lại cho đến khi thực thể bị loại khỏi ngữ cảnh và được tải lại:

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. Lọc các thực thể với @Where

Giả sử chúng ta muốn cung cấp một điều kiện bổ sung cho truy vấn bất cứ khi nào chúng ta yêu cầu một thực thể nào đó.

Ví dụ, chúng ta cần thực hiện "xóa mềm". Điều này có nghĩa là thực thể không bao giờ bị xóa khỏi cơ sở dữ liệu mà chỉ được đánh dấu là đã xóa bằng trường boolean .

Chúng tôi sẽ phải hết sức cẩn thận với tất cả các truy vấn hiện tại và tương lai trong ứng dụng. Chúng tôi phải cung cấp điều kiện bổ sung này cho mọi truy vấn. May mắn thay, Hibernate cung cấp một cách để thực hiện việc này ở một nơi:

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

Các @Where chú thích trên một phương pháp có chứa một điều khoản SQL sẽ được thêm vào bất kỳ thắc mắc hoặc subquery để tổ chức này:

employee.setDeleted(true); session.flush(); session.clear(); employee = session.find(Employee.class, employee.getId()); assertThat(employee).isNull();

Như trong trường hợp chú thích @Formula , vì chúng ta đang xử lý SQL thô, điều kiện @Where sẽ không được đánh giá lại cho đến khi chúng ta chuyển thực thể vào cơ sở dữ liệu và loại bỏ nó khỏi ngữ cảnh .

Cho đến thời điểm đó, thực thể sẽ ở trong ngữ cảnh và sẽ có thể truy cập được bằng các truy vấn và tra cứu bằng id .

Các @Where chú thích cũng có thể được sử dụng cho một lĩnh vực sưu tập. Giả sử chúng ta có một danh sách các điện thoại có thể xóa:

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

Sau đó, từ phía Nhân viên , chúng tôi có thể lập bản đồ một bộ sưu tập các điện thoại có thể xóa được như sau:

public class Employee implements Serializable { // ... @OneToMany @JoinColumn(name = "employee_id") @Where(clause = "deleted = false") private Set phones = new HashSet(0); }

Sự khác biệt là bộ sưu tập Employee.phones sẽ luôn được lọc, nhưng chúng tôi vẫn có thể lấy tất cả các điện thoại, kể cả những điện thoại đã xóa, thông qua truy vấn trực tiếp:

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. Lọc tham số với @Filter

Vấn đề với chú thích @Where là nó cho phép chúng tôi chỉ xác định một truy vấn tĩnh mà không có tham số và nó không thể bị vô hiệu hóa hoặc kích hoạt theo yêu cầu.

Các @Filter chú thích hoạt động theo cách tương tự như @Where , nhưng nó cũng có thể được kích hoạt hay vô hiệu hóa trên mức độ phiên, và cũng có tham số.

5.1. Định nghĩa @Filter

Để chứng minh cách hoạt động của @Filter , trước tiên hãy thêm định nghĩa bộ lọc sau vào thực thể Nhân viên :

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

Các @FilterDef chú thích định nghĩa tên bộ lọc và một bộ các thông số của nó mà sẽ tham gia vào truy vấn. Kiểu của tham số là tên của một trong các kiểu Hibernate (Type, UserType hoặc CompositeUserType), trong trường hợp của chúng ta, là một int .

Chú thích @FilterDef có thể được đặt ở loại hoặc ở cấp gói. Lưu ý rằng nó không xác định điều kiện lọc bản thân (mặc dù chúng ta có thể xác định defaultCondition tham số).

Điều này có nghĩa là chúng ta có thể xác định bộ lọc (tên và tập hợp các tham số) ở một nơi và sau đó xác định các điều kiện cho bộ lọc ở nhiều nơi khác một cách khác nhau.

Điều này có thể được thực hiện với chú thích @Filter . Trong trường hợp của chúng tôi, chúng tôi đặt nó trong cùng một lớp cho đơn giản. Cú pháp của điều kiện là một SQL thô với tên tham số đứng trước dấu hai chấm.

5.2. Truy cập các thực thể được lọc

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;

Ở đây chúng tôi đã chỉ định loại cột entity_type ( chuỗi ), loại cột entity_id ( int ), các giá trị được chấp nhận trong cột entity_type ( “Nhân viên”“Điện thoại” ) và các loại thực thể tương ứng.

Bây giờ, giả sử chúng ta có một nhân viên với hai chiếc điện thoại được mô tả như sau:

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

Giờ đây, chúng tôi có thể thêm siêu dữ liệu mô tả cho cả ba thực thể, mặc dù chúng có các loại không liên quan khác nhau:

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. Kết luận

Trong bài viết này, chúng tôi đã khám phá một số chú thích của Hibernate cho phép tinh chỉnh ánh xạ thực thể bằng cách sử dụng SQL thô.

Mã nguồn của bài viết hiện có trên GitHub.