a dev's blog

Some thoughts about thoughts.

Eine schöne Mapped-Superclass mit Spring-Data und JPA

2016-02-25 Development Java Spring

Intro

Object-relational-mapping ist eine schöne Sache und gibt dem Entwickler richtige, echte Objekte an die Hand, wenn er die Datenbank abfragt. Meist ist es auch noch so, dass wir gerne noch irgendwelche Meta-Informationen an unseren Datensätzen haben. Ich persönlich möchte in jedem Fall wissen, welcher User etwas bei mir in der Datenbank aktualisiert oder erstellt hat - und vor allem möchte ich wissen, wann das passiert ist.

JPA (die Java Persistence API) gibt uns da schon einiges an die Hand. Wir können Annotations wie PrePersist vergeben um unser Ziel zu erreichen, aber noch angenehmer wird es, wenn wir uns ein paar Features aus Spring-Data bedienen.

Setup

Ich habe wie immer ein kleines Beispielprojekt eingerichtet. Du findest es auf meinem Github Account. Diesmal habe ich auch einen JUnit-Test ergänzt, der die Funktionalität nach und nach demonstriert.

Als erstes steht ein Spring-Boot-Projekt mit einer einzigen Dependency, nämlich:

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.3.2.RELEASE</version>
  <relativePath/>
</parent>

<!-- ... -->

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- ... -->

Oh du schöner User, jeder kennt dich…

… und da jeder von uns weiß was ein User ist, nehme ich ihn an dieser Stelle als Beispiel. Ein User-Objekt sollte, ich denke da sind wir uns einig, zumindest über einen Usernamen sowie ein Passwort verfügen, right? Daher legen wir uns jetzt folgende Entity an:

@Entity
@Table("t_user")
public class User implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;

  @Column(length = 64)
  private String username;

  @Column(length = 64)
  private String password;

  // ... Getter and setter.
}

Neben Usernamen und Passwort siehst du auch noch eine automatisch generierte ID, die wir aber im nächsten Schritt auslagern werden. Im Allgemeinen lässt sich sagen, dass diese Entität ist an sich eigentlich voll funktionsfähig ist und mittels JPA genutzt werden kann.

Die Anlage einer Mapped-Superclass

Aber eigentlich haben wir die o.g. ID vermutlich in allen unseren JPA-Entities - und nicht nur in unserer User-Entity. Daher lagern wir die Gemeinsamkeiten im nächsten Schritt in eine separate Klasse namens BaseEntity aus.

@MappedSuperclass
public abstract class BaseEntity implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;

  // Getter and setter.
}

Die Klasse BaseEntity bekommt die Annotation @MappedSuperclass. Damit wird klar, dass die Klasse selbst nicht persistiert wird, ihre Eigenschaft allerdings an die sie implementierenden Kinder abtritt. Damit wird jetzt auch unsere User-Entity schlanker.

@Entity
@Table("t_user")
public class User extends BaseEntity {

  @Column(length = 64)
  private String username;

  @Column(length = 64)
  private String password;

  // ... Getter and setter.
}

Spring-Data Annotations wie für eine Mapped-Superclass gemacht

Spring-Data bringt - neben jeder Menge anderer großartiger Features - auch komplett fertige Annotations für JPA-Auditing mit. Das heißt, dass vor allen schreibenden Datenbankoperationen eine Reihe von Annotations geprüft werden, die wiederum darüber entscheiden ob mit dem annotierten Feld etwas besonderes vor dem Schreibprozess passieren soll. Die Annotations, von denen ich rede sind:

  • @CreatedDate: Speichert die aktuelle Zeit bei Erstellung des Datensatzes.
  • @CreatedBy: Speichert einen Hinweis auf den User, der den Datensatz erstellt hat.
  • @LastModifiedDate: Speichert die aktuelle Zeit bei der letzten Änderung des Datensatzes.
  • @LastModifiedBy: Speichert einen Hinweis auf den User, der die letzte Änderung des Datensatzes vollzogen hat.

Wenn wir diese Annotations, mit den passenden Feldern, in unsere BaseEntity einbauen, erhalten wir folgendes:

@MappedSuperclass
@EntityListeners({AuditingEntityListener.class})
public abstract class BaseEntity implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;

  @CreatedDate
  @Temporal(TemporalType.TIMESTAMP)
  private Date createdDate;

  @CreatedBy
  private String createdBy;

  @LastModifiedDate
  @Temporal(TemporalType.TIMESTAMP)
  private Date lastModifiedDate;

  @LastModifiedBy
  private String lastModifiedBy;

  // ... Getter and setter.
}

Wichtig

In Zeile 2 registrieren wir einen Listener, der für alle Datenbankoperationen gilt. Der Listener verfügt wiederum über einen Handler - um genauer zu sein eine Bean vom Typ AuditorAware. Nur wenn dieser Handler auch wirklich vorhanden ist, werden die Persist-Hooks von JPA durchlaufen. Diese AuditorAware-Bean legen wir im folgenden Abschnitt an.

Anschalten des Auditings

Damit die Spring-Data-Features funktionieren, muss man das JPA-Auditing anschalten. Dafür gibt es in Spring-Boot - wie wir es gewohnt sind - eine schicke Annotation @EnableJpaAuditing. Außerdem brauchen wir eine Bean vom Typ AuditorAware. Diese Bean liefert uns in diesem Beispiel den Usernamen des aktuell angemeldeten Users, der die aktuelle Datenbank-Operationen durchführt. In meinem Beispiel ist es allerdings immer nur der gute alte Administrator.

@Configuration
@EnableJpaAuditing
public class DatabaseConfiguration {

  @Bean
  public AuditorAware<String> auditorAware() {
    return () -> "Administrator";
  }
}

Wichtiger Hinweis

Man kann in der AuditorAware-Bean durchaus auch echte Datenbank-User verwenden und diese in der MappedSuperclass verwenden. Dazu sollte man allerdings nicht auf die Idee kommen innerhalb des Lambdas der AuditorAware-Bean die Datenbank nach einem User zu fragen, sondern diesen beispielsweise über den SecurityContextHolder aus Spring-Security heranziehen. Als Hintergrund zu dem Tip: Beim jeder Datenbank Operation wird mit dem Anschalten des JPA-Auditings ein Interceptor durchlaufen, der wiederum die AuditorAware-Bean nach dem gerade angemeldeten User fragt. Damit würdest du in einem StackOverflow landen - und wer will das schon. ;)

Fazit

Jetzt hat jede Entity die von unserem MappedSupertype erbt, ein paar Felder mehr und diese können uns helfen Fehler zu entdecken und Operationen nachzuvollziehen.

Cheers!