Benutzerdefinierte Typen im Ruhezustand und die @ Typ-Anmerkung

1. Übersicht

Der Ruhezustand vereinfacht die Datenverarbeitung zwischen SQL und JDBC, indem das objektorientierte Modell in Java dem relationalen Modell in Datenbanken zugeordnet wird. Obwohl die Zuordnung grundlegender Java-Klassen in Hibernate integriert ist, ist die Zuordnung benutzerdefinierter Typen häufig komplex.

In diesem Tutorial werden wir sehen, wie Hibernate es uns ermöglicht, die grundlegende Typzuordnung auf benutzerdefinierte Java-Klassen zu erweitern. Darüber hinaus werden einige gängige Beispiele für benutzerdefinierte Typen angezeigt und mithilfe des Typzuordnungsmechanismus von Hibernate implementiert.

2. Zuordnungstypen im Ruhezustand

Hibernate verwendet Zuordnungstypen zum Konvertieren von Java-Objekten in SQL-Abfragen zum Speichern von Daten. In ähnlicher Weise werden Zuordnungstypen zum Konvertieren von SQL ResultSet in Java-Objekte beim Abrufen von Daten verwendet.

Im Allgemeinen kategorisiert Hibernate die Typen in Entitätstypen und Werttypen . Insbesondere werden Entitätstypen verwendet, um domänenspezifische Java-Entitäten zuzuordnen, und sind daher unabhängig von anderen Typen in der Anwendung vorhanden. Im Gegensatz dazu werden Werttypen stattdessen zum Zuordnen von Datenobjekten verwendet und gehören fast immer den Entitäten.

In diesem Tutorial konzentrieren wir uns auf die Zuordnung von Werttypen, die weiter unterteilt sind in:

  • Grundtypen - Zuordnung für grundlegende Java-Typen
  • Embeddable - Zuordnung für zusammengesetzte Java-Typen / POJOs
  • Sammlungen - Zuordnung für eine Sammlung von einfachen und zusammengesetzten Java-Typen

3. Maven-Abhängigkeiten

Um unsere benutzerdefinierten Hibernate-Typen zu erstellen, benötigen wir die Hibernate-Core-Abhängigkeit:

 org.hibernate hibernate-core 5.3.6.Final 

4. Benutzerdefinierte Typen im Ruhezustand

Wir können für die meisten Benutzerdomänen grundlegende Zuordnungstypen für den Ruhezustand verwenden. Es gibt jedoch viele Anwendungsfälle, in denen ein benutzerdefinierter Typ implementiert werden muss.

Der Ruhezustand erleichtert die Implementierung benutzerdefinierter Typen relativ. Es gibt drei Ansätze zum Implementieren eines benutzerdefinierten Typs im Ruhezustand. Lassen Sie uns jeden von ihnen im Detail besprechen.

4.1. Implementieren von Basictype

Wir können einen benutzerdefinierten Basistyp erstellen, indem wir den BasicType von Hibernate oder eine seiner spezifischen Implementierungen, AbstractSingleColumnStandardBasicType, implementieren .

Bevor wir unseren ersten benutzerdefinierten Typ implementieren, sehen wir uns einen allgemeinen Anwendungsfall für die Implementierung eines Basistyps an. Angenommen, wir müssen mit einer Legacy-Datenbank arbeiten, in der Daten als VARCHAR gespeichert sind. Normalerweise ordnet Hibernate dies dem Java-Typ String zu . Dadurch wird die Datumsüberprüfung für Anwendungsentwickler schwieriger.

Implementieren wir also unseren LocalDateString- Typ, der den LocalDate- Java-Typ als VARCHAR speichert :

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

Das Wichtigste in diesem Code sind die Konstruktorparameter. Erstens handelt es sich um eine Instanz von SqlTypeDescriptor , der SQL- Typdarstellung von Hibernate, die in unserem Beispiel VARCHAR ist. Das zweite Argument ist eine Instanz von JavaTypeDescriptor, die den Java-Typ darstellt.

Jetzt können wir einen LocalDateStringJavaDescriptor zum Speichern und Abrufen von LocalDate als VARCHAR implementieren :

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

Als nächstes müssen wir außer Kraft zu setzen Wrap und unwrap Methoden zur Umwandlung des Java - Typs in SQL. Beginnen wir mit dem Auspacken:

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

Als nächstes die Wrap- Methode:

@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 () wird während der PreparedStatement- Bindung aufgerufen , um LocalDate in einen String-Typ zu konvertieren , der VARCHAR zugeordnet ist. Ebenso wird wrap () beim Abrufen von ResultSet aufgerufen , um String in ein Java LocalDate zu konvertieren .

Schließlich können wir unseren benutzerdefinierten Typ in unserer Entitätsklasse verwenden:

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

Später werden wir sehen, wie wir diesen Typ im Ruhezustand registrieren können. Und als Ergebnis, beziehen sich auf diese Art mit dem Registrierungsschlüssel anstelle der vollständig qualifizierten Klassennamen.

4.2. Implementieren von Usertype

Bei der Vielzahl der Basistypen im Ruhezustand ist es sehr selten, dass wir einen benutzerdefinierten Basistyp implementieren müssen. Im Gegensatz dazu besteht ein typischerer Anwendungsfall darin, ein komplexes Java-Domänenobjekt der Datenbank zuzuordnen. Solche Domänenobjekte werden im Allgemeinen in mehreren Datenbankspalten gespeichert.

Implementieren wir also ein komplexes PhoneNumber- Objekt, indem wir UserType implementieren :

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 } 

Hier gibt die überschriebene sqlTypes- Methode die SQL-Feldtypen in derselben Reihenfolge zurück, in der sie in unserer PhoneNumber- Klasse deklariert sind . In ähnlicher Weise returnedClass liefert Methode unseren Phone Java - Typen.

Sie müssen nur noch die Methoden zum Konvertieren zwischen Java-Typ und SQL-Typ implementieren, wie wir es für unseren BasicType getan haben .

Zunächst die nullSafeGet- Methode:

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

Als nächstes die nullSafeSet- Methode:

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

Schließlich können wir unseren benutzerdefinierten PhoneNumberType in unserer OfficeEmployee- Entitätsklasse deklarieren :

@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. CompositeUserType implementieren

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

In diesem Lernprogramm wurden mehrere Ansätze zum Definieren eines benutzerdefinierten Typs im Ruhezustand erläutert. Darüber hinaus haben wir einige benutzerdefinierte Typen für unsere Entitätsklasse implementiert, die auf einigen gängigen Anwendungsfällen basieren, in denen ein neuer benutzerdefinierter Typ nützlich sein kann.

Wie immer sind die Codebeispiele auf GitHub verfügbar.