Spring Boot 2 Datenlayer mit JPA

Generischer i18n fähiger Datenlayer mit Auditing


In einem Spring-Boot 2 Projekt wird Hibernate eingebunden, sobald man den JPA-Starter als Dependency definiert. Hibernate implementiert die JPA-Spezifikation. Somit können alle JPA Features, insbesondere die Annotationen verwendet werden. Hiermit hat man eine sehr gute Basis für die Arbeit mit Datenbanken. Mit dieser guten Basis kann man ziemlich viel machen. Ein Beispiel dafür ist ein i18-fähiger generischer Datenlayer. Mit etwas Geschick lässt sich das ganz ähnlich in PHP z.B. mit Doctrine implementieren.

Um Getter und Setter nicht selber schreiben zu müssen wurde Lombok verwendet. Und um Änderungen nachverfolgen zu können wird das Auditing verwendet, das bereits im JPA-Helper enthalten ist. Die meiste Arbeit ist eigentlich damit getan eine intelligente Vererbungshierarchie zu implementieren, die sich wie folgt darstellt. Es ist geplant das ganze Projekt auf github zur Verfügung zu stellen... Irgendwann, hehe.

Abstrakte Basisklasse für alle Entitäten


package de.finait.start.app.model.base;

import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

import com.fasterxml.jackson.annotation.JsonView;

import de.finait.start.app.view.Views;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
@MappedSuperclass
public abstract class AbstractEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonView(Views.Public.class)
    private Long id;

    @Override
    public int hashCode() {
        return this.getClass().getName().hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;

        String theType = this.getClass().getName();
        Class<?> theClass;
        try {
            theClass = Class.forName(theType);
            Object other = theClass.cast(obj);

            return id != null && id.equals(((AbstractEntity) other).getId());

        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }
}

Ein weiteres interessantes Feature ist das Auditing. Hiermit kann man speichern wann der Datensatz erstellt wurde, durch wen und wann er geändert wurde durch wen. Somit geht es weiter in der Vererbungshierarchie.

Abstrakte Basisklasse, die das Auditing-Feature hinzufügt


package de.finait.start.app.model.base;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.Instant;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditable<U> extends AbstractEntity {

    @CreatedBy
    @Column(name = "created_by")
    private U createdBy;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedBy
    @Column(name = "last_modified_by")
    private U lastModifiedBy;

    @LastModifiedDate
    @Column(name = "last_modified_at")
    private Instant lastModifiedAt;
}

Nun kann je nach Einsatzzweck gewählt werden von welcher Klasse man seine Entitäten ableitet. Wird kein Auditing gebraucht nimmt man die Klasse AbstractEntity. Andernfalls nimmt man AbstractAuditable. Eine Ableitung von AbstractEntity ist selbsterklärend. Darum als nächstes wie eine Ableitung von AbstractAuditable aussieht. Hier wird eine Entität für ein Land dargestellt. Sie erbt alle Attribute der Elternklassen.

Ableitung von AbstractAuditable


package de.finait.start.app.model;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

import com.fasterxml.jackson.annotation.JsonView;

import de.finait.start.app.model.base.AbstractAuditable;
import de.finait.start.app.view.Views;
import de.finait.start.appuser.model.Address;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
@Entity
@Table(name = "Country")
public class Country extends AbstractAuditable<String> {

    @NotBlank
    @Size(min = 3, max = 3)
    @Column(name = "iso", nullable = false, length = 3, unique = true)
    @JsonView(Views.Public.class)
    private String iso;

    @NotBlank
    @Size(min = 2, max = 2)
    @Column(name = "code", nullable = false, length = 2, unique = true)
    @JsonView(Views.Public.class)
    private String code;

    @NotBlank
    @Size(min = 2, max = 50)
    @Column(name = "name", nullable = false, length = 50, unique = true)
    @JsonView(Views.Public.class)
    private String name;

}

Natürlich ist es auch schön die Mehrsprachigkeit so weit wie möglich zu automatisieren. Hierzu wird folgendes Interface mit zwei default Methoden verwendet. Damit werden Übersetzungen aus einer entsprechenden i18n Entität geladen.

Interface für das Laden der Übersetzungen


package de.finait.start.app.model.base;

import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;

import org.springframework.context.i18n.LocaleContextHolder;

public interface Translateable<T extends AbstractI18nEntity> {

    List<T> getTranslations();

    default T getTranslation() {
    	Locale locale = LocaleContextHolder.getLocale();
        return getTranslations().stream().filter((T item) -> item.getLocale().equals(locale))
                .collect(Collectors.toList()).get(0);
    }

    default T getTranslation(Locale locale) {
        return getTranslations().stream().filter((T item) -> item.getLocale().equals(locale))
                .collect(Collectors.toList()).get(0);
    }
}

Weiterhin wird eine abstrakte Klasse erstellt, die als Basis für Entitäten dient mit Eigenschaften, die übersetzt werden müssen.

Basisklasse für Übersetzungsentitäten


package de.finait.start.app.model.base;

import java.util.Locale;

import javax.persistence.Column;
import javax.persistence.Convert;
import javax.persistence.MappedSuperclass;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

import com.fasterxml.jackson.annotation.JsonGetter;
import com.fasterxml.jackson.annotation.JsonView;

import de.finait.start.app.converter.LocaleToStringConverter;
import de.finait.start.app.view.Views;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
@MappedSuperclass
public abstract class AbstractI18nEntity extends AbstractEntity {

    @NotBlank
    @Size(min = 5, max = 5)
    @Column(name = "locale", nullable = false, length = 5)
    @Convert(converter = LocaleToStringConverter.class)
    @JsonView(Views.Public.class)
    private Locale locale;

    @JsonGetter("locale")
    public String getLocaleIETFBCP47() {
        return this.locale.toLanguageTag();
    }

}

Natürlich gibt es davon auch eine Auditable-Version

Basisklasse für Übersetzungsentitäten mit Auditable-Feature


package de.finait.start.app.model.base;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.Instant;

import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;

@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class AbstractAuditableI18n<U> extends AbstractI18nEntity {

    @CreatedBy
    @Column(name = "created_by")
    private U createdBy;

    @CreatedDate
    @Column(name = "created_at", nullable = false, updatable = false)
    private Instant createdAt;

    @LastModifiedBy
    @Column(name = "last_modified_by")
    private U lastModifiedBy;

    @LastModifiedDate
    @Column(name = "last_modified_at")
    private Instant lastModifiedAt;

}

Nun hat man alles für ein Auditing sowie einfaches i18n-Handling. Man muss nur noch die Eigenschaften hinzufügen, die spezifisch für die Entität sind und eine Entität für Übersetzungen erstellen. Attribute, die keiner Übersetzung bedürfen, kommen in die "Basis Entität" z.B. Gender. Attribute, die übersetzt werden müssen, kommen in die GenderI18n Entität. Im Folgenden wie eine Implementierung mit Übersetzungen aussieht.

Basisklasse, z.B. Gender


package de.finait.start.app.model;

import java.util.List;

import javax.persistence.Entity;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonView;

import de.finait.start.app.model.base.AbstractAuditable;
import de.finait.start.app.model.base.Translateable;
import de.finait.start.app.view.Views;
import de.finait.start.appuser.model.AppUserSetting;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
@Entity
@Table(name = "gender")
public class Gender extends AbstractAuditable<String> implements Translateable<GenderI18n> {

    @OneToMany(mappedBy="i18nParent")
    @JsonView(Views.Public.class)
    private List<GenderI18n> translations;

    //hier keine weiteren Attribute, da alle übersetzt werden müssen. Nur eine Relation.
    @OneToMany(mappedBy="gender")
    private List<AppUserSetting> userSettings;

}

Übersetzungsklasse, z.B. GenderI18n


package de.finait.start.app.model;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

import com.fasterxml.jackson.annotation.JsonView;

import de.finait.start.app.model.base.AbstractAuditableI18n;
import de.finait.start.app.view.Views;

@Getter(AccessLevel.PUBLIC)
@Setter(AccessLevel.PUBLIC)
@Entity
@Table(name = "gender_i18n", uniqueConstraints = @UniqueConstraint(columnNames = { "gender_id", "locale" }))
public class GenderI18n extends AbstractAuditableI18n<String> {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "gender_id")
    private Gender i18nParent;

    @NotBlank
    @Size(min = 3, max = 20)
    @Column(name = "name", nullable = false, length = 20)
    @JsonView(Views.Public.class)
    private String name;

    @NotBlank
    @Size(min = 3, max = 20)
    @Column(name = "short_name", nullable = false, length = 20)
    @JsonView(Views.Public.class)
    private String shortName;

}

So, schon hat man sich ziemlich viel Schreibarbeit in der Zukunft gespart :-)

Zurück zur Kategorie: Spring
Zurück nach oben