Pemetaan Dinamik dengan Hibernate

1. Pengenalan

Dalam artikel ini, kita akan meneroka beberapa keupayaan pemetaan dinamik Hibernate dengan @Formula , @Where , @Filter dan @Any penjelasan.

Perhatikan bahawa walaupun Hibernate menerapkan spesifikasi JPA, anotasi yang dijelaskan di sini hanya tersedia di Hibernate dan tidak langsung dibawa ke pelaksanaan JPA lain.

2. Penyediaan Projek

Untuk menunjukkan ciri, kami hanya memerlukan perpustakaan inti hibernate dan pangkalan data H2 yang disokong:

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

Untuk versi perpustakaan inti hibernate terkini , pergi ke Maven Central.

3. Lajur yang Dikira Dengan @Formula

Katakan kita ingin mengira nilai medan entiti berdasarkan beberapa sifat lain. Salah satu cara untuk melakukannya adalah dengan menentukan bidang baca sahaja yang dihitung dalam entiti 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 bahawa kita harus melakukan pengiraan semula setiap kali kita mengakses medan maya ini oleh pengambil .

Akan lebih mudah untuk mendapatkan nilai yang sudah dikira dari pangkalan data. Ini boleh dilakukan dengan penjelasan @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 pangkalan data asli dan prosedur yang tersimpan dan pada dasarnya melakukan apa sahaja yang tidak melanggar sintaks klausa pilih SQL untuk bidang ini.

Hibernate cukup pintar untuk menguraikan SQL yang kami sediakan dan memasukkan alias jadual dan medan yang betul. Perhatian yang perlu diberi perhatian adalah bahawa kerana nilai anotasi adalah SQL mentah, ini mungkin membuat pemetaan kami bergantung pada pangkalan data.

Juga, ingat bahawa nilainya dikira semasa entiti diambil dari pangkalan data . Oleh itu, apabila kita meneruskan atau mengemas kini entiti, nilainya tidak akan dikira semula sehingga entiti diusir dari konteks dan dimuat semula:

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. Menyaring Entiti Dengan @Where

Anggaplah kami ingin memberikan syarat tambahan untuk pertanyaan setiap kali kami meminta beberapa entiti.

Sebagai contoh, kita perlu menerapkan "soft delete". Ini bermaksud entiti tidak pernah dihapus dari pangkalan data, tetapi hanya ditandai sebagai dihapus dengan medan boolean .

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

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

The @Where anotasi pada kaedah yang mengandungi klausa SQL yang akan ditambah untuk sebarang pertanyaan atau subquery kepada entiti ini:

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

Seperti dalam kes anotasi @Formula , kerana kita berurusan dengan SQL mentah, keadaan @Where tidak akan dapat dinilai semula sehingga kita memasukkan entiti ke pangkalan data dan mengusirnya dari konteks .

Sehingga masa itu, entiti akan tetap berada dalam konteks dan akan dapat diakses dengan pertanyaan dan carian melalui id .

The @Where anotasi juga boleh digunakan untuk medan koleksi. Katakan kita mempunyai senarai telefon yang boleh dipadamkan:

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

Kemudian, dari pihak Karyawan , kami dapat memetakan koleksi telefon yang boleh dipadamkan seperti berikut:

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

Perbezaannya adalah bahawa koleksi Employee.phone akan selalu disaring, tetapi kami masih dapat mendapatkan semua telefon, termasuk yang dihapus, melalui pertanyaan 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. Penapisan Parameter Dengan @Filter

Masalah dengan @Where anotasi adalah bahawa ia membolehkan kita hanya menentukan pertanyaan statik tanpa parameter, dan tidak dapat dinonaktifkan atau diaktifkan berdasarkan permintaan.

The @Filter anotasi berfungsi dengan cara yang sama seperti @Where , tetapi ia juga boleh didayakan atau dilumpuhkan pada tahap sesi, dan juga parameterized.

5.1. Mendefinisikan @Filter

Untuk menunjukkan bagaimana @Filter berfungsi, mari kita tambahkan definisi penapis berikut ke entiti Pekerja :

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

The @FilterDef anotasi mentakrifkan nama penapis dan satu set parameter yang akan mengambil bahagian dalam pertanyaan itu. Jenis parameter adalah nama salah satu jenis Hibernate (Type, UserType atau CompositeUserType), dalam kes kami, int .

Anotasi @FilterDef boleh diletakkan sama ada pada jenis atau pada tahap pakej. Perhatikan bahawa ia tidak menentukan keadaan penapis itu sendiri (walaupun kita dapat menentukan parameter defaultCondition ).

Ini bermaksud bahawa kita dapat menentukan penapis (namanya dan set parameter) di satu tempat dan kemudian menentukan syarat untuk penapis di beberapa tempat lain secara berbeza.

Ini boleh dilakukan dengan penjelasan @Filter . Dalam kes kami, kami meletakkannya di kelas yang sama untuk kesederhanaan. Sintaks keadaan adalah SQL mentah dengan nama parameter yang didahului oleh titik dua.

5.2. Mengakses Entiti yang Disaring

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 kami telah menentukan jenis lajur entiti_jenis ( rentetan ), jenis lajur entiti_id ( int ), nilai yang dapat diterima di lajur entiti_jenis ( "Karyawan" dan "Telefon" ) dan jenis entiti yang sesuai.

Sekarang, anggap kita mempunyai seorang pekerja dengan dua telefon 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 untuk ketiga-tiga entiti, walaupun mereka mempunyai jenis yang tidak berkaitan:

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

Dalam artikel ini, kami telah meneroka beberapa anotasi Hibernate yang membenarkan pemetaan entiti penyesuaian menggunakan SQL mentah.

Kod sumber untuk artikel tersebut terdapat di GitHub.