Benutzerdefinierte Typen im Ruhezustand

1. Überblick

Der Ruhezustand vereinfacht die Datenverarbeitung zwischen SQL und JDBC, indem das objektorientierte Modell in Java dem relationalen Modell in Datenbanken zugeordnet wird.

Obwohl das Mapping von grundlegenden Java-Klassen in Hibernate integriert ist, ist das Mapping von benutzerdefinierten Typen oft komplex.

In diesem Lernprogramm erfahren Sie, wie wir mit Hibernate die grundlegende Typenzuordnung auf benutzerdefinierte Java-Klassen erweitern können. Darüber hinaus sehen wir einige gängige Beispiele für benutzerdefinierte Typen und implementieren sie mithilfe des Typmechanismus von Hibernate.

2. Hibernate-Zuordnungstypen

Im Ruhezustand werden Mapping-Typen zum Konvertieren von Java-Objekten in SQL-Abfragen zum Speichern von Daten verwendet. In ähnlicher Weise verwendet es Zuordnungstypen zum Konvertieren von SQL ResultSet in Java-Objekte, während Daten abgerufen werden.

Im Allgemeinen kategorisiert Hibernate die Typen in Entitätstypen und Werttypen . Entitätstypen werden insbesondere zur Zuordnung domänenspezifischer Java-Entitäten verwendet und sind daher unabhängig von anderen Typen in der Anwendung vorhanden. Im Gegensatz dazu werden Wertetypen stattdessen zum Abbilden von Datenobjekten verwendet und gehören fast immer den Entitäten.

In diesem Tutorial konzentrieren wir uns auf das Mapping von Value-Typen, die weiter unterteilt sind:

  • Basistypen - Zuordnung für grundlegende Java-Typen

  • Embeddable - Mapping für zusammengesetzte Java-Typen/POJOs

  • Collections - Mapping für eine Sammlung von grundlegendem und zusammengesetztem Java

Art

  1. Maven-Abhängigkeiten

Um unsere benutzerdefinierten Hibernate-Typen erstellen zu können, benötigen Sie die Abhängigkeit von hibernate-core :

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>5.3.6.Final</version>
</dependency>

4. Benutzerdefinierte Typen im Ruhezustand

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

Der Ruhezustand macht es relativ einfacher, benutzerdefinierte Typen zu implementieren. Es gibt drei Ansätze zum Implementieren eines benutzerdefinierten Typs in Hibernate. Lassen Sie uns jeden einzelnen ausführlich besprechen.

4.1. BasicType implementieren

Wir können einen benutzerdefinierten Basistyp erstellen, indem Sie _BasicType von Hibernate für eine seiner spezifischen Implementierungen AbstractSingleColumnStandardBasicType._ implementieren.

Bevor wir unseren ersten benutzerdefinierten Typ implementieren, sehen wir einen allgemeinen Anwendungsfall für die Implementierung eines Basistyps. Angenommen, wir müssen mit einer älteren Datenbank arbeiten, die Datumsangaben als VARCHAR speichert. Normalerweise würde Hibernate dies dem String -Java-Typ zuordnen. Dadurch wird die Validierung des Datums für Anwendungsentwickler schwieriger.

Lassen Sie uns also unseren __LocalDateString type implementieren, der LocalDate __Java-Typ als VARCHAR speichert:

public class LocalDateStringType
  extends AbstractSingleColumnStandardBasicType<LocalDate> {

    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 ist dies eine Instanz von SqlTypeDescriptor , der Hibernate-Darstellung des SQL-Typs, 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<LocalDate> {

    public static final LocalDateStringJavaDescriptor INSTANCE =
      new LocalDateStringJavaDescriptor();

    public LocalDateStringJavaDescriptor() {
        super(LocalDate.class, ImmutableMutabilityPlan.INSTANCE);
    }

   //other methods
}

Als Nächstes müssen wir _wrap und unwrap methods überschreiben, um den Java-Typ in SQL zu konvertieren. Beginnen wir mit dem unwrap: _

@Override
public <X> X unwrap(LocalDate value, Class<X> 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 <X> 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 PreparedStatement binden aufgerufen, um LocalDate _ in einen String-Typ zu konvertieren, der VARCHAR zugeordnet ist.

Ebenso wird _wrap () während ResultSet retrieval aufgerufen, um String in ein Java LocalDate_ zu konvertieren.

Schließlich können wir unseren benutzerdefinierten Typ in unserer Entity-Klasse 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 in Hibernate registrieren können. Daher beziehen sich ** auf diesen Typ mit dem Registrierungsschlüssel anstelle des vollständig qualifizierten Klassennamens.

4.2. UserType implementieren

Bei der Vielfalt der Basistypen in Hibernate ist es sehr selten, dass ein benutzerdefinierter Basistyp implementiert werden muss. Im Gegensatz dazu ist ein typischerer Anwendungsfall das Zuordnen eines komplexen Java-Domänenobjekts zu der Datenbank. Solche Domänenobjekte werden im Allgemeinen in mehreren Datenbankspalten gespeichert.

Lassen Sie uns ein komplexes PhoneNumber -Objekt implementieren, indem Sie 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 Methode __sqlTypes die SQL-Feldtypen in derselben Reihenfolge zurück, in der sie in unserer Klasse PhoneNumber deklariert sind. In ähnlicher Weise gibt returnedClass method unseren PhoneNumber __Java-Typ zurück.

Jetzt müssen Sie nur noch die Methoden implementieren, die zwischen Java-Typ und SQL-Typ konvertiert werden sollen, wie bei unserem BasicType .

Zuerst 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);
    } 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 __entity-Klasse angeben:

@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

Die Implementierung von _UserType funktioniert gut für unkomplizierte Typen. Das Mapping komplexer Java-Typen (mit Collections- und Cascaded-Composite-Typen) erfordert jedoch mehr Komplexität. Der Ruhezustand ermöglicht die Zuordnung solcher Typen durch Implementierung der Schnittstelle CompositeUserType _ .

Lassen Sie uns dies in Aktion sehen, indem Sie einen __AddressType für den zuvor verwendeten OfficeEmployee __entity implementieren:

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
}

Im Gegensatz zu UserTypes , das den Index der Typmerkmale abbildet, geben _CompositeType maps Eigenschaftennamen unserer Address _ -Klasse ein. Noch wichtiger ist, dass die Methode getPropertyType die Methode für jede Eigenschaft zurückgibt.

Außerdem müssen wir _getPropertyValue und setPropertyValue methods implementieren, um PreparedStatement und ResultSet indexes auf die Eigenschaft type abzubilden. Als Beispiel betrachten wir getPropertyValue für unseren 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());
}

Schließlich müssen wir __nullSafeGet und nullSafeSet methoden für die Konvertierung zwischen Java- und SQL-Typen implementieren. Dies ähnelt dem, was wir zuvor in unserem PhoneNumberType gemacht haben.

Bitte beachten Sie, dass CompositeType ’s im Allgemeinen als alternativer Zuordnungsmechanismus für __Embeddable __types implementiert werden.

4.4. Typparametrierung

Mit Hibernate können Sie nicht nur benutzerdefinierte Typen erstellen, sondern auch das Verhalten von Typen basierend auf Parametern ändern.

Nehmen wir beispielsweise an, wir müssen den __Salary für unseren __OfficeEmployee speichern. Wichtiger ist, dass der Antrag den Gehaltsbetrag in örtliche Landeswährung umrechnen muss.

Lassen Sie uns unseren parametrisierten _SalaryType implementieren, der currency _ als Parameter akzeptiert:

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
}

Bitte beachten Sie, dass wir die CompositeUserType -Methoden aus unserem Beispiel übersprungen haben, um uns auf die Parametrisierung zu konzentrieren. Hier haben wir einfach DynamicParameterizedType von Hibernate implementiert und die Methode _setParameterValues ​​() überschrieben. Nun akzeptiert der SalaryType einen currency _ -Parameter und konvertiert einen beliebigen Betrag, bevor er gespeichert wird.

Wir übergeben den currency als Parameter, während wir den Salary deklarieren:

@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. Grundtyp-Registrierung

Hibernate verwaltet die Zuordnung aller integrierten Basistypen im BasicTypeRegistry . Dadurch entfällt die Notwendigkeit, Zuordnungsinformationen für solche Typen zu kommentieren.

Außerdem können Sie mit Hibernate benutzerdefinierte Typen wie Basistypen im BasicTypeRegistry registrieren. Normalerweise registrieren Anwendungen einen benutzerdefinierten Typ, während __SessionFactory bootstrapping ist. Verstehen Sie dies, indem Sie den zuvor implementierten __LocalDateString-Typ registrieren:

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
}

Daher entfällt die Einschränkung der Verwendung des vollständig qualifizierten Klassennamens in der Typzuordnung:

@Entity
@Table(name = "OfficeEmployee")
public class OfficeEmployee {

    @Column
    @Type(type = "LocalDateString")
    private LocalDate dateOfJoining;

   //other methods
}

Hier ist _LocalDateString der Schlüssel, dem der LocalDateStringType _ zugeordnet ist.

Alternativ können Sie die Typregistrierung überspringen, indem Sie TypeDefs definieren:

@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. Schlussfolgerung

In diesem Lernprogramm wurden mehrere Ansätze zum Definieren eines benutzerdefinierten Typs in Hibernate erläutert. Darüber hinaus ** haben wir einige benutzerdefinierte Typen für unsere Entitätsklasse implementiert, die auf häufigen Anwendungsfällen basieren, bei denen ein neuer benutzerdefinierter Typ hilfreich sein kann.