Pemetaan Dinamis dengan Hibernate

1. Perkenalan

Dalam artikel ini, kita akan menjelajahi beberapa kapabilitas pemetaan dinamis Hibernate dengan anotasi @Formula , @Where , @Filter dan @Any .

Perhatikan bahwa meskipun Hibernate mengimplementasikan spesifikasi JPA, anotasi yang dijelaskan di sini hanya tersedia dalam Hibernate dan tidak langsung portabel ke implementasi JPA lainnya.

2. Pengaturan Proyek

Untuk mendemonstrasikan fitur-fiturnya, kita hanya membutuhkan pustaka hibernate-core dan basis data H2 pendukung:

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

Untuk versi pustaka inti hibernasi saat ini , kunjungi Maven Central.

3. Kolom Terhitung Dengan @Formula

Misalkan kita ingin menghitung nilai bidang entitas berdasarkan beberapa properti lainnya. Salah satu cara untuk melakukannya adalah dengan menentukan bidang hanya baca terhitung di entitas Java kami:

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

Kelemahan yang jelas adalah kita harus melakukan penghitungan ulang setiap kali kita mengakses bidang virtual ini oleh pengambil .

Akan jauh lebih mudah untuk mendapatkan nilai yang sudah dihitung dari database. Ini dapat dilakukan dengan anotasi @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; }

Dengan @Formula , kita dapat menggunakan subkueri, memanggil fungsi database asli dan prosedur tersimpan dan pada dasarnya melakukan apa pun yang tidak merusak sintaks klausa pemilihan SQL untuk bidang ini.

Hibernate cukup pintar untuk mengurai SQL yang kami sediakan dan memasukkan alias tabel dan kolom yang benar. Peringatan yang harus diperhatikan adalah karena nilai anotasi adalah SQL mentah, ini mungkin membuat pemetaan kita bergantung pada database.

Selain itu, perlu diingat bahwa nilai dihitung saat entitas diambil dari database . Oleh karena itu, saat kami mempertahankan atau memperbarui entitas, nilainya tidak akan dihitung ulang hingga entitas tersebut dikeluarkan dari konteks dan dimuat lagi:

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. Memfilter Entitas Dengan @Where

Misalkan kami ingin memberikan kondisi tambahan ke kueri setiap kali kami meminta entitas.

Misalnya, kita perlu menerapkan "hapus lunak". Artinya entitas tidak pernah dihapus dari database, tetapi hanya ditandai sebagai dihapus dengan kolom boolean .

Kami harus sangat berhati-hati dengan semua pertanyaan yang ada dan yang akan datang dalam aplikasi. Kami harus memberikan ketentuan tambahan ini untuk setiap kueri. Untungnya, Hibernate menyediakan cara untuk melakukan ini di satu tempat:

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

The @Where penjelasan pada metode berisi klausul SQL yang akan ditambahkan ke permintaan atau subquery untuk entitas ini:

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

Seperti dalam kasus anotasi @Formula , karena kita berurusan dengan SQL mentah, kondisi @Where tidak akan dievaluasi ulang sampai kita membuang entitas ke database dan mengeluarkannya dari konteks .

Hingga saat itu, entitas akan tetap berada dalam konteks dan akan dapat diakses dengan kueri dan pencarian berdasarkan id .

The @Where penjelasan juga dapat digunakan untuk bidang koleksi. Misalkan kita memiliki daftar telepon yang dapat dihapus:

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

Kemudian, dari sisi Employee , kita bisa memetakan kumpulan ponsel yang bisa dihapus sebagai berikut:

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

Perbedaannya adalah koleksi telepon Karyawan akan selalu difilter, tetapi kami masih bisa mendapatkan semua telepon, termasuk yang dihapus, melalui kueri langsung:

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. Pemfilteran Parameter Dengan @Filter

Masalah dengan anotasi @Where adalah memungkinkan kita untuk hanya menentukan kueri statis tanpa parameter, dan tidak dapat dinonaktifkan atau diaktifkan oleh permintaan.

The @Filter penjelasan bekerja dengan cara yang sama seperti @Where , tetapi juga dapat diaktifkan atau dinonaktifkan pada tingkat sesi, dan juga parameterized.

5.1. Mendefinisikan @Filter

Untuk mendemonstrasikan cara kerja @Filter , pertama-tama mari tambahkan definisi filter berikut ke entitas Karyawan :

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

The @FilterDef penjelasan mendefinisikan nama filter dan satu set parameter yang akan berpartisipasi dalam query. Jenis parameter adalah nama salah satu jenis Hibernate (Type, UserType atau CompositeUserType), dalam kasus kami, int .

Anotasi @FilterDef dapat ditempatkan pada tipe atau pada tingkat paket. Perhatikan bahwa ini tidak menentukan kondisi filter itu sendiri (meskipun kita bisa menentukan parameter defaultCondition ).

Ini berarti bahwa kita dapat menentukan filter (nama dan kumpulan parameternya) di satu tempat dan kemudian menentukan kondisi untuk filter di beberapa tempat lain secara berbeda.

Ini dapat dilakukan dengan anotasi @Filter . Dalam kasus kami, kami meletakkannya di kelas yang sama untuk kesederhanaan. Sintaks dari kondisi ini adalah SQL mentah dengan nama parameter yang diawali dengan titik dua.

5.2. Mengakses Entitas yang Difilter

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;

Di sini kita telah menentukan jenis kolom entity_type ( string ), jenis kolom entity_id ( int ), nilai yang dapat diterima di kolom entity_type ( "Karyawan" dan "Telepon" ), dan jenis entity yang sesuai.

Sekarang, misalkan kita memiliki seorang karyawan dengan dua telepon yang dijelaskan seperti ini:

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

Sekarang kita dapat menambahkan metadata deskriptif ke ketiga entitas tersebut, meskipun mereka memiliki jenis yang tidak terkait:

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

Dalam artikel ini, kami telah menjelajahi beberapa anotasi Hibernate yang memungkinkan penyesuaian pemetaan entitas menggunakan SQL mentah.

Kode sumber artikel tersedia di GitHub.