Jenis Kustom dalam Hibernate dan @Type Annotation

1. Ikhtisar

Hibernate menyederhanakan penanganan data antara SQL dan JDBC dengan memetakan model Berorientasi Objek di Jawa dengan model Relasional di Database. Meskipun pemetaan kelas Java dasar sudah ada di dalam Hibernate, pemetaan jenis kustom seringkali rumit.

Dalam tutorial ini, kita akan melihat bagaimana Hibernate memungkinkan kita untuk memperluas pemetaan tipe dasar ke kelas Java khusus. Selain itu, kita juga akan melihat beberapa contoh umum dari jenis kustom dan menerapkannya menggunakan mekanisme pemetaan jenis Hibernate.

2. Jenis Pemetaan Hibernasi

Hibernate menggunakan tipe pemetaan untuk mengubah objek Java menjadi kueri SQL untuk menyimpan data. Demikian pula, ia menggunakan jenis pemetaan untuk mengubah SQL ResultSet menjadi objek Java saat mengambil data.

Umumnya, Hibernate mengkategorikan tipe ke dalam Tipe Entitas dan Tipe Nilai . Secara khusus, tipe Entitas digunakan untuk memetakan entitas Java spesifik domain dan karenanya, ada secara independen dari tipe lain dalam aplikasi. Sebaliknya, Tipe Nilai digunakan untuk memetakan objek data dan hampir selalu dimiliki oleh Entitas.

Dalam tutorial ini, kami akan fokus pada pemetaan jenis Nilai yang selanjutnya diklasifikasikan menjadi:

  • Tipe Dasar - Pemetaan untuk tipe Java dasar
  • Embeddable - Pemetaan untuk tipe java komposit / POJO
  • Koleksi - Pemetaan untuk koleksi tipe java dasar dan komposit

3. Ketergantungan Maven

Untuk membuat tipe Hibernate kustom kami, kami memerlukan dependensi hibernate-core:

 org.hibernate hibernate-core 5.3.6.Final 

4. Jenis Kustom dalam Hibernasi

Kita dapat menggunakan tipe pemetaan dasar Hibernate untuk sebagian besar domain pengguna. Namun, ada banyak kasus penggunaan, di mana kita perlu mengimplementasikan tipe kustom.

Hibernate membuatnya relatif lebih mudah untuk menerapkan tipe kustom. Ada tiga pendekatan untuk menerapkan tipe adat di Hibernate. Mari kita bahas masing-masing secara rinci.

4.1. Menerapkan BasicType

Kita dapat membuat tipe dasar kustom dengan mengimplementasikan BasicType Hibernate atau salah satu implementasi spesifiknya, AbstractSingleColumnStandardBasicType.

Sebelum kita menerapkan tipe kustom pertama kita, mari kita lihat kasus penggunaan umum untuk mengimplementasikan tipe dasar. Misalkan kita harus bekerja dengan database lama, yang menyimpan tanggal sebagai VARCHAR. Biasanya, Hibernate akan memetakan ini ke tipe String Java. Dengan demikian, membuat validasi tanggal lebih sulit bagi pengembang aplikasi.

Jadi mari kita terapkan tipe LocalDateString kita , yang menyimpan tipe Java LocalDate sebagai VARCHAR:

public class LocalDateStringType extends AbstractSingleColumnStandardBasicType { public static final LocalDateStringType INSTANCE = new LocalDateStringType(); public LocalDateStringType() { super(VarcharTypeDescriptor.INSTANCE, LocalDateStringJavaDescriptor.INSTANCE); } @Override public String getName() { return "LocalDateString"; } }

Hal terpenting dalam kode ini adalah parameter konstruktor. Pertama, adalah turunan dari SqlTypeDescriptor , yang merupakan representasi tipe SQL Hibernate, yang VARCHAR sebagai contoh kami. Dan, argumen kedua adalah turunan dari JavaTypeDescriptor yang mewakili tipe Java.

Sekarang, kita dapat mengimplementasikan LocalDateStringJavaDescriptor untuk menyimpan dan mengambil LocalDate sebagai VARCHAR:

public class LocalDateStringJavaDescriptor extends AbstractTypeDescriptor { public static final LocalDateStringJavaDescriptor INSTANCE = new LocalDateStringJavaDescriptor(); public LocalDateStringJavaDescriptor() { super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE); } // other methods }

Selanjutnya, kita perlu mengganti metode wrap and unwrap untuk mengubah tipe Java menjadi SQL. Mari kita mulai dengan pembukaannya:

@Override public  X unwrap(LocalDate value, Class type, WrapperOptions options) { if (value == null) return null; if (String.class.isAssignableFrom(type)) return (X) LocalDateType.FORMATTER.format(value); throw unknownUnwrap(type); }

Selanjutnya, metode bungkus :

@Override public  LocalDate wrap(X value, WrapperOptions options) { if (value == null) return null; if(String.class.isInstance(value)) return LocalDate.from(LocalDateType.FORMATTER.parse((CharSequence) value)); throw unknownWrap(value.getClass()); }

unwrap () dipanggil selama pengikatan PreparedStatement untuk mengubah LocalDate menjadi tipe String, yang dipetakan ke VARCHAR. Demikian juga, wrap () dipanggil selama pengambilan ResultSet untuk mengubah String menjadi Java LocalDate .

Akhirnya, kita bisa menggunakan tipe kustom kita di kelas Entity kita:

@Entity @Table(name = "OfficeEmployee") public class OfficeEmployee { @Column @Type(type = "com.baeldung.hibernate.customtypes.LocalDateStringType") private LocalDate dateOfJoining; // other fields and methods }

Nanti, kita akan melihat bagaimana kita dapat mendaftarkan tipe ini di Hibernate. Dan sebagai hasilnya, rujuk ke jenis ini menggunakan kunci pendaftaran, bukan nama kelas yang sepenuhnya memenuhi syarat.

4.2. Menerapkan UserType

Dengan variasi tipe dasar dalam Hibernate, sangat jarang kami perlu menerapkan tipe dasar kustom. Sebaliknya, kasus penggunaan yang lebih umum adalah memetakan objek domain Java yang kompleks ke database. Objek domain seperti itu umumnya disimpan dalam beberapa kolom database.

Jadi, mari mengimplementasikan objek PhoneNumber yang kompleks dengan mengimplementasikan UserType:

public class PhoneNumberType implements UserType { @Override public int[] sqlTypes() { return new int[]{Types.INTEGER, Types.INTEGER, Types.INTEGER}; } @Override public Class returnedClass() { return PhoneNumber.class; } // other methods } 

Di sini, metode sqlTypes yang diganti mengembalikan jenis bidang SQL, dalam urutan yang sama seperti yang dideklarasikan di kelas PhoneNumber kami . Demikian pula, returnedClass metode mengembalikan kami PhoneNumber jenis Java.

Satu-satunya hal yang harus dilakukan adalah mengimplementasikan metode untuk mengonversi antara tipe Java dan tipe SQL, seperti yang kami lakukan untuk BasicType kami .

Pertama, metode nullSafeGet :

@Override public Object nullSafeGet(ResultSet rs, String[] names, SharedSessionContractImplementor session, Object owner) throws HibernateException, SQLException { int countryCode = rs.getInt(names[0]); if (rs.wasNull()) return null; int cityCode = rs.getInt(names[1]); int number = rs.getInt(names[2]); PhoneNumber employeeNumber = new PhoneNumber(countryCode, cityCode, number); return employeeNumber; }

Selanjutnya, metode nullSafeSet :

@Override public void nullSafeSet(PreparedStatement st, Object value, int index, SharedSessionContractImplementor session) throws HibernateException, SQLException { if (Objects.isNull(value)) { st.setNull(index, Types.INTEGER); st.setNull(index + 1, Types.INTEGER); st.setNull(index + 2, Types.INTEGER); } else { PhoneNumber employeeNumber = (PhoneNumber) value; st.setInt(index,employeeNumber.getCountryCode()); st.setInt(index+1,employeeNumber.getCityCode()); st.setInt(index+2,employeeNumber.getNumber()); } }

Akhirnya, kami dapat mendeklarasikan PhoneNumberType kustom kami di kelas entitas OfficeEmployee kami :

@Entity @Table(name = "OfficeEmployee") public class OfficeEmployee { @Columns(columns = { @Column(name = "country_code"), @Column(name = "city_code"), @Column(name = "number") }) @Type(type = "com.baeldung.hibernate.customtypes.PhoneNumberType") private PhoneNumber employeeNumber; // other fields and methods }

4.3. Mengimplementasikan CompositeUserType

Implementing UserType works well for straightforward types. However, mapping complex Java types (with Collections and Cascaded composite types) need more sophistication. Hibernate allows us to map such types by implementing the CompositeUserType interface.

So, let's see this in action by implementing an AddressType for the OfficeEmployee entity we used earlier:

public class AddressType implements CompositeUserType { @Override public String[] getPropertyNames() { return new String[] { "addressLine1", "addressLine2", "city", "country", "zipcode" }; } @Override public Type[] getPropertyTypes() { return new Type[] { StringType.INSTANCE, StringType.INSTANCE, StringType.INSTANCE, StringType.INSTANCE, IntegerType.INSTANCE }; } // other methods }

Contrary to UserTypes, which maps the index of the type properties, CompositeType maps property names of our Address class. More importantly, the getPropertyType method returns the mapping types for each property.

Additionally, we also need to implement getPropertyValue and setPropertyValue methods for mapping PreparedStatement and ResultSet indexes to type property. As an example, consider getPropertyValue for our AddressType:

@Override public Object getPropertyValue(Object component, int property) throws HibernateException { Address empAdd = (Address) component; switch (property) { case 0: return empAdd.getAddressLine1(); case 1: return empAdd.getAddressLine2(); case 2: return empAdd.getCity(); case 3: return empAdd.getCountry(); case 4: return Integer.valueOf(empAdd.getZipCode()); } throw new IllegalArgumentException(property + " is an invalid property index for class type " + component.getClass().getName()); }

Finally, we would need to implement nullSafeGet and nullSafeSet methods for conversion between Java and SQL types. This is similar to what we did earlier in our PhoneNumberType.

Please note that CompositeType‘s are generally implemented as an alternative mapping mechanism to Embeddable types.

4.4. Type Parameterization

Besides creating custom types, Hibernate also allows us to alter the behavior of types based on parameters.

For instance, suppose that we need to store the Salary for our OfficeEmployee. More importantly, the application must convert the salary amountinto geographical local currency amount.

So, let's implement our parameterized SalaryType which accepts currency as a parameter:

public class SalaryType implements CompositeUserType, DynamicParameterizedType { private String localCurrency; @Override public void setParameterValues(Properties parameters) { this.localCurrency = parameters.getProperty("currency"); } // other method implementations from CompositeUserType }

Please note that we have skipped the CompositeUserType methods from our example to focus on parameterization. Here, we simply implemented Hibernate's DynamicParameterizedType, and override the setParameterValues() method. Now, the SalaryType accept a currency parameter and will convert any amount before storing it.

We'll pass the currency as a parameter while declaring the Salary:

@Entity @Table(name = "OfficeEmployee") public class OfficeEmployee { @Type(type = "com.baeldung.hibernate.customtypes.SalaryType", parameters = { @Parameter(name = "currency", value = "USD") }) @Columns(columns = { @Column(name = "amount"), @Column(name = "currency") }) private Salary salary; // other fields and methods }

5. Basic Type Registry

Hibernate maintains the mapping of all in-built basic types in the BasicTypeRegistry. Thus, eliminating the need to annotate mapping information for such types.

Additionally, Hibernate allows us to register custom types, just like basic types, in the BasicTypeRegistry. Normally, applications would register custom type while bootstrapping the SessionFactory. Let's understand this by registering the LocalDateString type we implemented earlier:

private static SessionFactory makeSessionFactory() { ServiceRegistry serviceRegistry = StandardServiceRegistryBuilder() .applySettings(getProperties()).build(); MetadataSources metadataSources = new MetadataSources(serviceRegistry); Metadata metadata = metadataSources.getMetadataBuilder() .applyBasicType(LocalDateStringType.INSTANCE) .build(); return metadata.getSessionFactoryBuilder().build() } private static Properties getProperties() { // return hibernate properties }

Thus, it takes away the limitation of using the fully qualified class name in Type mapping:

@Entity @Table(name = "OfficeEmployee") public class OfficeEmployee { @Column @Type(type = "LocalDateString") private LocalDate dateOfJoining; // other methods }

Here, LocalDateString is the key to which the LocalDateStringType is mapped.

Alternatively, we can skip Type registration by defining TypeDefs:

@TypeDef(name = "PhoneNumber", typeClass = PhoneNumberType.class, defaultForType = PhoneNumber.class) @Entity @Table(name = "OfficeEmployee") public class OfficeEmployee { @Columns(columns = {@Column(name = "country_code"), @Column(name = "city_code"), @Column(name = "number")}) private PhoneNumber employeeNumber; // other methods }

6. Conclusion

Dalam tutorial ini, kami membahas beberapa pendekatan untuk menentukan tipe kustom dalam Hibernate. Selain itu, kami menerapkan beberapa tipe kustom untuk kelas entitas kami berdasarkan beberapa kasus penggunaan umum di mana tipe kustom baru dapat berguna.

Seperti biasa, contoh kode tersedia di GitHub.