Jenis Tersuai di Hibernate dan Anotasi Jenis @

1. Gambaran keseluruhan

Hibernate mempermudah pengendalian data antara SQL dan JDBC dengan memetakan model Object Oriented di Java dengan model Relational dalam Pangkalan Data. Walaupun pemetaan kelas Java asas ada di Hibernate, pemetaan jenis kustom sering rumit.

Dalam tutorial ini, kita akan melihat bagaimana Hibernate membolehkan kita memperluas pemetaan jenis asas ke kelas Java khusus. Selain itu, kami juga akan melihat beberapa contoh umum dari jenis khusus dan menerapkannya menggunakan mekanisme pemetaan jenis Hibernate.

2. Jenis Pemetaan Hibernate

Hibernate menggunakan jenis pemetaan untuk menukar objek Java menjadi pertanyaan SQL untuk menyimpan data. Begitu juga, ia menggunakan jenis pemetaan untuk menukar SQL ResultSet menjadi objek Java sambil mengambil data.

Secara amnya, Hibernate mengkategorikan jenis menjadi Jenis Entiti dan Jenis Nilai . Secara khusus, jenis Entity digunakan untuk memetakan entitas Java khusus domain dan oleh itu, wujud secara bebas daripada jenis lain dalam aplikasi. Sebaliknya, Jenis Nilai digunakan untuk memetakan objek data dan hampir selalu dimiliki oleh Entiti.

Dalam tutorial ini, kita akan memfokuskan pada pemetaan jenis Nilai yang selanjutnya dikelaskan kepada:

  • Jenis Asas - Pemetaan untuk jenis Java asas
  • Embeddable - Pemetaan untuk jenis java komposit / POJO
  • Koleksi - Pemetaan untuk koleksi jenis java asas dan komposit

3. Pergantungan Maven

Untuk membuat jenis Hibernate tersuai kami, kami memerlukan pergantungan inti hibernate:

 org.hibernate hibernate-core 5.3.6.Final 

4. Jenis Custom di Hibernate

Kita boleh menggunakan jenis pemetaan asas Hibernate untuk kebanyakan domain pengguna. Namun, ada banyak kasus penggunaan, di mana kita perlu menerapkan jenis khusus.

Hibernate menjadikannya lebih mudah untuk melaksanakan jenis tersuai. Terdapat tiga pendekatan untuk menerapkan jenis kebiasaan di Hibernate. Mari kita bincangkan masing-masing secara terperinci.

4.1. Melaksanakan Jenis Asas

Kita boleh membuat jenis asas tersuai dengan menerapkan Hibernate's BasicType atau salah satu pelaksanaannya yang spesifik, AbstractSingleColumnStandardBasicType.

Sebelum kita melaksanakan jenis khusus pertama kita, mari kita lihat kes penggunaan umum untuk melaksanakan jenis asas. Katakan kita harus bekerjasama dengan pangkalan data lama, yang menyimpan tarikh sebagai VARCHAR. Biasanya, Hibernate akan memetakan ini ke jenis String Java. Dengan demikian, menjadikan pengesahan tarikh lebih sukar bagi pembangun aplikasi.

Oleh itu mari kita laksanakan jenis LocalDateString kami , yang menyimpan jenis 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"; } }

Perkara yang paling penting dalam kod ini adalah parameter pembina. Pertama, adalah contoh SqlTypeDescriptor , yang merupakan perwakilan jenis SQL Hibernate, yang merupakan VARCHAR untuk contoh kita. Dan, argumen kedua adalah contoh JavaTypeDescriptor yang mewakili jenis Java.

Sekarang, kita dapat melaksanakan 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 }

Seterusnya, kita perlu mengesampingkan kaedah wrap dan unrap untuk menukar jenis Java menjadi SQL. Mari mulakan dengan unrap:

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

Seterusnya, kaedah 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()); }

undrap () dipanggil semasa mengikat PreparedStatement untuk menukar LocalDate ke jenis String, yang dipetakan ke VARCHAR. Begitu juga, wrap () dipanggil semasa pengambilan ResultSet untuk menukar String ke Java LocalDate .

Akhirnya, kami dapat menggunakan jenis khusus kami di kelas Entity kami:

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

Later, we'll see how we can register this type in Hibernate. And as a result, refer to this type using the registration key instead of the fully qualified class name.

4.2. Implementing UserType

With the variety of basic types in Hibernate, it is very rare that we need to implement a custom basic type. In contrast, a more typical use case is to map a complex Java domain object to the database. Such domain objects are generally stored in multiple database columns.

So let's implement a complex PhoneNumber object by implementing 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 } 

Here, the overridden sqlTypes method returns the SQL types of fields, in the same order as they are declared in our PhoneNumber class. Similarly, returnedClass method returns our PhoneNumber Java type.

The only thing left to do is to implement the methods to convert between Java type and SQL type, as we did for our BasicType.

First, the nullSafeGet method:

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

Next, the nullSafeSet method:

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

Finally, we can declare our custom PhoneNumberType in our OfficeEmployee entity class:

@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. Implementing 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 membincangkan pelbagai pendekatan untuk menentukan jenis khusus di Hibernate. Selain itu, kami menerapkan beberapa jenis khusus untuk kelas entiti kami berdasarkan beberapa kes penggunaan biasa di mana jenis khusus baru dapat berguna.

Seperti biasa, contoh kod boleh didapati di GitHub.