Implementieren der Finanzverwaltung mit JPA (Teil 3)

Dieser Beitrag beschreibt die Implementierung der Schnittstelle aus dem letzten Teil. Zur Speicherung dient eine relationale Datenbank – H2. Auf sie greift das Programm mittels der Java Persistente API (JPA) in der “Geschmacksrichtung” Spring-Data-JPA zu.

Implementieren der Datenbankanbindung

Das Modell mit JPA-Annotationen implementieren

Für die Speicherung der Daten in eine relationale Datenbank kommt die Java Persistente API (JPA) zum Einsatz. Die Abbildung der Model-Klassen in ein Datenbank-Schema erfolgt dabei mittels Annotationen.

@Data
@Entity
public class Buchung {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String verwendung;
	private String empfaenger;
	
	@ToString.Exclude
	@OneToMany(mappedBy = "buchung", cascade = CascadeType.ALL, 
                      fetch = FetchType.EAGER, orphanRemoval = true)
	private List<Umsatz> umsaetze = new ArrayList<>();
	
	public void addUmsatz(Umsatz umsatz) {
		getUmsaetze().add(umsatz);
	}
}

@Data
@Entity
public class Umsatz {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private LocalDate valuta;
	private BigDecimal betrag;
	
	@ManyToOne
	@JoinColumn(name = "konto_id", nullable = false,
                    updatable = false)
	private Konto konto;
	
	@ManyToOne
	@JoinColumn(name = "buchung_id", nullable = false, 
                    updatable = false)
	private Buchung buchung;
}

@Data
@Entity
public class Konto {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String name;
	private BigDecimal saldo = BigDecimal.ZERO;
	
	@OneToMany(mappedBy = "konto")
	private List<Umsatz> umsaetze = new ArrayList<>();

	public void verbuche(Umsatz umsatz) {
		umsaetze.add(umsatz);
		saldo = saldo.add(umsatz.getBetrag());
	}
}

Technisches Setup

Das Umfeld und die Zugriffe ist durch Spring-Data-JPA einfach zu implementieren. Die Umsetzung ist analog des JPA-Guides implementiert.

Als Datenbank dient H2, die als Abhängigkeit im Projekt eingebunden ist. Sie steht damit im Klassenpfad. Der Spring-Data-Starter sorgt ohne zusätzliche Konfiguration erst mal für eine In-Memory-Datenbank.

In der Datei application.properties ist die Eigenschaft spring.jpa.hibernate.ddl-auto=update gesetzt. Spring-Data verwendete Hiberante, das bei jedem Start das Datenbankschema prüft. Da die In-Memory-Datenbank nach dem Start leer ist, entsteht hier immer ein vollständige Schema.

Die Datenbankzugriffe implementieren

Die eigentlichen Zugriffe implementieren Repository-Interfaces, die CrudRepository von Spring-Data erweitern. Typische Zugriffe, wie Speichen, alle Daten laden, Löschen, etc. sind damit durch den Framework gegeben.

Spring Data ermöglicht das Erstellen von Abfragen auf Attribute, indem einer Abfrage-Methode eine Konventionen der Benennung einhält. Konten sollen per Name zu finden sein. Diese einfache Abfrage ist gegeben durch:

Konto findByName(String name);

Einzig das Laden der Buchungen je Konto ist etwas komplexer, deshalb ist ein Ansatz via Konvention der Benennung hier nicht sinnvoll. Hier hilft eine @Query-Annotation mit einer Abfrage, die in JPAs Quere-Language definiert ist.

public interface BuchungRepository extends CrudRepository<Buchung, String>{
	@Query(
	 "select b from Buchung b "
	 + "join b.umsaetze u "
	+ "where u.konto.name = :kontoName "
	+ "order by u.valuta desc, b.id desc ")
	List<Buchung> findByKontoName(String kontoName);
}

Implemtentieren der Service-Klassen

Lesende Funktionen

Die Controller-Klasse des letzten Teil delegiert alle Logik an Service-Klassen. Die lesenden Anwendungsfälle setzten die Repository-Interfaces um, an die Service-Klassen Abfragen einfach nur weiterleiten. Die Service-Ebene als Zwischenstation hat allerdings Vorteile.

  • Service-Klassen sind zentraler Ansatzpunkt, an dem Transaktionsverhalten definiert werden kann.
  • Die API-Ebene kann auf zusätzliche Importe von Repositories verichten.

Schreibende Funktion: buchen

Die Kern-Funktion ist somit im BuchungService das buchen, das letztlich Jahrhunderte alte Praxis der doppelten Buchführung technisch umsetzt:

  1. Der Buchführende trägt Geschäftsvorfälle laufend in ein Journal (Grundbuch) ein. (Anwendungsfall “Buchung erstellen”)
  2. Anschließend erfolgt ein Übertrag der Kontoumsätze je Konto in das Hauptbuch, wodurch die Bewegungen auf einem Konto sichtbar sind (ermöglicht den Anwendungsfall “Umsatz anzeigen”)
  3. Abschließend erfolgt die Saldierung der Konten und der aktuelle Kontostand ist auf den Konten nachvollziehbar (Anwendungsfall “Konten abfragen”)

Die Reihenfolge der Bearbeitung ist dabei insbesondere der Abhängigkeiten der Datenbank geschuldet.

@Transactional	
public void buche(Buchung buchung) {
  for (Umsatz umsatz : buchung.getUmsaetze()) {
    			
    Konto konto = kontoRepository.findByName(umsatz.getKonto().getName());
    if (isNull(konto)) {
      konto = kontoRepository.save(umsatz.getKonto());
      konto.setSaldo(BigDecimal.ZERO);
    }
    umsatz.setKonto(konto);
    umsatz.setBuchung(buchung);
    konto.getUmsaetze().add(umsatz);

    konto.setSaldo(konto.getSaldo().add(umsatz.getBetrag()));

    kontoRepository.save(konto);
  }
  buchungRepository.save(buchung);	
}

Zunächst erfolgt das Buchen in einer Transaktion. Ich spreche alle Konten an oder mache keine Änderung. Es kommt so nicht zu einer plötzlichen Geldvermehrung, ohne dass auch jemand zahlt.

Die Verarbeitung arbeitet zunächst alle in der übergebenen Buchung genannten Umsätze ab.
Zu jedem Umsatz der übergebenen Buchung sucht die Methode zunächst ein Konto mit dem genannten Namen. Sollte es nicht existieren, so wird dieses Konto angelegt.
Als nächstes entstehen die bidirektionalen Relationen zwischen den Entitäten Buchung-Umsatz-Konto. Sie sind in der übergebenen Struktur einseitig schon vorhanden, benötigen aber noch die technische Gegenrichtung.

Abschließend berechnet und setzt die buchen-Methode den neuen Saldo des Kontos und speichert es.

Nach der Verarbeitung ist nun noch die Buchung selbst zu speichern. Dabei wandern auch die zugehörigen Umsätze in der Datenbank, da diese im JPA-Mapping kaskadierend an die Buchung gebunden sind.

finally

Damit liegt eine erste, einfache Implementierung vor. Sie ist mehr aus einem Reflex entstanden, der im beruflichen Kontext geübt ist. Gibt es nicht noch einen weiteren Weg, um eine Finanzverwaltung für den Heimgebrauch zu implementieren? Doch die gibt es. Dazu im nächsten Teil mehr. Anschließend möchte ich beide Implementierungen vergleichen, und den vielversprechenderen Weg weiter implementieren.

Happy coding!

Ein Gedanke zu „Implementieren der Finanzverwaltung mit JPA (Teil 3)“

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.