Die Sourcen des DBFSample finden sich wie immer im GitHub.
Das DBFSample ist ein PoC um eine DBF Datei mit Java verarbeiten zu können.
Im Projekt haben wir einige DBF Dateien erhalten, deren Daten wir importieren/verarbeiten müssen. Das soll nicht meine Aufgabe sein, aber ich möchte für den Fall vorbereitet sein, dass ich dabei unterstützen darf.
Ich brauche also erstmal nur verstehen, was eine DBF Datei ist und wie ich grundlegend damit arbeiten kann.
Was ist eine DBF Datei
Eine DBF-Datei ist eine Standarddatenbankdatei, die von dBASE, einer Datenbankverwaltungssystemanwendung, verwendet wird. Es organisiert Daten in mehreren Datensätzen mit Feldern, die in einem Array-Datentyp gespeichert sind.
Aufgrund der frühzeitigen Einführung in der Datenbank und einer relativ einfachen Dateistruktur wurden DBF-Dateien allgemein als Standardspeicherformat für strukturierte Daten in kommerziellen Anwendungen akzeptiert.
https://datei.wiki/extension/dbf
Wie kann ich eine DBF Datei öffnen?
DBeaver
Da es sich um ein Datenbankformat handelt und ich grade das Tool DBeaver in meinen Arbeitsalltag eingeführt habe, lag es für mich nahe, die Datei mit DBeaver zu öffnen.
Dazu musste ich einen Treiber zu DBeaver hinzufügen um anschließend die Datei öffnen zu können. Ich konnte dann die Tabellenstruktur sehen, aber nicht auf die Tabelle zugreifen. Es gab eine Fehlermeldung, dass eine weitere Datei fehlen würde.
java.sql.SQLException: nl.knaw.dans.common.dbflib.CorruptedTableException: Could not find file 'C:\dev\tmp\adress.dbt' (or multiple matches for the file)
DBeaver Stack-Trace
Diese andere Datei gibt es nicht und sie ist auch nicht für den Zugriff erforderlich, wie der erfolgreiche Zugriff über die anderen Wege beweist.
Etwas ausführlicher hatte ich es im Artikel zu DBeaver geschrieben.
Excel
Excel öffnen, DBF Datei reinziehen, Daten ansehen. Fertig, so einfach kann es gehen.
Ich hatte mich allerdings durch die Bezeichnung Standarddatenbankdatei ablenken lassen, so dass ich zuerst die Wege über DBeaver und Java versucht hatte.
Java
Für den Zugriff mit Java habe ich die Bibliothek JavaDBF verwendet.
Die beiden Testklassen JavaDBFReaderTest und JavaDBFReaderWithFieldNamesTest waren schnell angepasst und eine weiter Klasse zum Auslesen aller Daten ReadItAll war dann auch problemlos möglich. Dabei ignoriere ich die Datentypen und lese einfach alles als Strings ein. Für den PoC reicht das.
DBF in PostgresDB speichern
Als Beispiel, wie ich mit den Daten weiterarbeiten kann, importiere ich sie in eine Postgres Datenbank.
Dazu lese ich zuerst die sample.dbf ein und erzeuge dann eine Tabelle sample mit allen Columns, die in sample.dbf vorhanden sind. Anschließend wird die Tabelle zeilenweise gefüllt.
Das meiste ist hardcodiert und die Spalten sind alles Text-Spalten, da ich die Datentypen aus der DBF Datei nicht auslese, aber für den PoC reicht das.
Mein Ziel war es, JPA konfigurativ ohne persistence.xml zu verwenden. Das habe ich nicht ganz geschafft, aber schon mal den Weg erarbeitet, wie es prinzipiell funktionieren könnte.
Hintergrund ist einfach der, dass ich in der persistence.xml die Konfiguration meiner Datenbank hinterlegen kann, aber wenn ich das war-File baue und auf den produktiven Server schiebe, dann möchte ich, dass diese Konfiguration durch die der produktiven Datenbank überschrieben werden kann.
Nach meiner Mittagspause hat sich der Test auf einmal anders verhalten und es wurde kein NoClassDefFoundError geschmissen, sondern ein ExceptionInInizlialisationError. Warum dem so ist 🤷♂️.
Beide Errors erweitern allerdings den LinkageError, also ist mein Test fix gefixt:
Bisher habe ich für den Datenbankzugriff mit einem proprietärem Framework gearbeitet, das ich jedoch für das aktuelle Projekt nicht verwenden kann. Bei der Wahl einer frei zugänglichen Alternative entschied ich mich für JPA, die Java/Jakarta Persistence API.
Die Datenbank
Als Datenbank benutze ich einfach das Setup aus meinem letzten Post.
Projekt Setup
Es wird ein neues Maven Projekt angelegt. Java Version 1.8.
Es wird die Javax Persistence API benötigt und eine Implementierung, hier: Hibernate. Als DB wird PostgreSQL verwendet, dazu wird der entsprechende Treiber benötigt.
Die pom.xml des Projekts:
4.0.0deringojpa0.0.1-SNAPSHOTJPATestJPA Test Project1.81.8UTF-8UTF-8javax.persistencejavax.persistence-api2.2org.hibernatehibernate-core5.6.1.Finalorg.postgresqlpostgresql42.2.18
Verbindungsbeschreibung
Die benötigten Informationen für den Verbindungsaufbau mit der DB werden in der persistence.xml hinterlegt:
Java Klassen
Die beiden Tabellen Adresse und Person werden jeweils in eine Java Klasse überführt. Dabei handelt es sich um POJOs mit Default Constructor, (generierter) toString, hashCode und equals Methoden. Annotation als Entity und für die ID, die uA objectID heißen soll und nicht wie in der DB object_id.
package deringo.jpa.entity;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Adresse implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "object_id")
private int objectID;
private String strasse;
private String ort;
public Adresse() {
// default constructor
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + objectID;
result = prime * result + ((ort == null) ? 0 : ort.hashCode());
result = prime * result + ((strasse == null) ? 0 : strasse.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Adresse other = (Adresse) obj;
if (objectID != other.objectID)
return false;
if (ort == null) {
if (other.ort != null)
return false;
} else if (!ort.equals(other.ort))
return false;
if (strasse == null) {
if (other.strasse != null)
return false;
} else if (!strasse.equals(other.strasse))
return false;
return true;
}
@Override
public String toString() {
return String.format("Adresse [objectID=%s, strasse=%s, ort=%s]", objectID, strasse, ort);
}
public int getObjectID() {
return objectID;
}
public void setObjectID(int objectID) {
this.objectID = objectID;
}
public String getStrasse() {
return strasse;
}
public void setStrasse(String strasse) {
this.strasse = strasse;
}
public String getOrt() {
return ort;
}
public void setOrt(String ort) {
this.ort = ort;
}
}
Für den Zugriff auf die Tabellen werden die jeweiligen Repository Klassen angelegt.
package deringo.jpa.repository;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import deringo.jpa.entity.Adresse;
public class AdresseRepository {
private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit");
public static Adresse getAdresseById(int id) {
EntityManager em = emf.createEntityManager();
return em.find(Adresse.class, id);
}
}
"Geschäftslogik" um zu testen, ob es funktioniert:
package deringo.jpa;
import deringo.jpa.entity.Adresse;
import deringo.jpa.repository.AdresseRepository;
public class TestMain {
public static void main(String[] args) {
int adresseID = 4;
Adresse adresse = AdresseRepository.getAdresseById(adresseID);
System.out.println(adresse);
}
}
Test Driven
Den Zugriff über die Repositories (und später auch Service Klassen) habe ich Test Driven entwickelt mit JUnit. Zur Entwicklung mit JUnit hatte ich schon mal einen Post verfasst.
Folgende Dependencies wurden der pom.xml hinzugefügt:
public static List getAdresseByOrt(String ort) {
EntityManager em = emf.createEntityManager();
TypedQuery query = em.createQuery("SELECT a FROM Adresse a WHERE a.ort = :ort", Adresse.class);
query.setParameter("ort", ort);
return query.getResultList();
}
Native Query
Um zB herauszufinden, wie die zuletzt vergebene ObjectID lautet, kann ein native Query verwendet werden:
public static int getLastObjectID() {
String sequenceName = "public.object_id_seq";
String sql = "SELECT s.last_value FROM " + sequenceName + " s";
EntityManager em = emf.createEntityManager();
BigInteger value = (BigInteger)em.createNativeQuery(sql).getSingleResult();
return value.intValue();
}
Kreuztabelle
Nehmen wir mal an, eine Person kann mehrere Adressen haben und an eine Adresse können mehrere Personen gemeldet sein.
Um das abzubilden benötigen wir zunächst eine Kreuztabelle, die wir in der DB anlegen:
DROP TABLE IF EXISTS public.adresse_person;
CREATE TABLE public.adresse_person (
adresse_object_id integer NOT NULL,
person_object_id integer NOT NULL
);
Solch eine Relation programmatisch anlegen:
public static void createAdressePersonRelation(int adresseId, int personId) {
String sql = "INSERT INTO adresse_person (adresse_object_id, person_object_id) VALUES (?, ?)";//, adresseId, personId);
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.createNativeQuery(sql)
.setParameter(1, adresseId)
.setParameter(2, personId)
.executeUpdate();
em.getTransaction().commit();
}
Die Adresse zu einer Person(enID) lässt sich ermitteln:
Das funktioniert nur, solange die Person nur eine Adresse hat.
Das kann man so machen, schöner ist es aber über entsprechend ausmodellierte ManyToMany Beziehungen in den Entities. Das Beispiel vervollständige ich hier erstmal nicht, da ich bisher es in meinem Projekt nur so wie oben beschrieben benötigte.
OneToMany
Wandeln wir obiges Beispiel mal ab: An einer Adresse können mehrere Personen gemeldet sein, aber eine Person immer nur an einer Adresse.
Wir fügen also der Person eine zusätzliche Spalte für die Adresse hinzu:
ALTER TABLE person ADD COLUMN adresse_object_id integer;
--
UPDATE person SET adresse_object_id = 4
public class Person implements Serializable {
[...]
@ManyToOne
@JoinColumn(name="adresse_object_id")
private Adresse adresse;
[...]
}
public class Adresse implements Serializable {
[..]
@OneToMany
@JoinColumn(name="adresse_object_id")
private List personen = new ArrayList<>();
[...]
}
Anschließend noch die Getter&Setter, toString, hashCode&equals neu generieren und einen Test ausführen:
Es soll das Objekt adresse ausgegeben werden, in welchem in der toString-Methode das Objekt person ausgegeben werden soll, in welchem das Objekt adresse ausgegeben werden, in welchem in der toString-Methode das Objekt person ausgegeben werden soll, in welchem das Objekt adresse ... usw.
Als Lösung muss die toString-Methode von Person händisch angepasst werden, so dass nicht mehr das Objekt adresse, sondern lediglich dessen ID ausgegeben wird:
Man möchte meinen, dass der Code zum löschen einer Adresse wie folgt lautet:
public static void deleteAdresse(Adresse adresse) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.remove(adresse);
em.getTransaction().commit();
}
Testen:
@Test
public void deleteAdresse() {
int adresseID = 8;
Adresse adresse = AdresseRepository.getAdresseById(adresseID);
assertNotNull(adresse);
AdresseRepository.deleteAdresse(adresse);
assertNull(adresse);
}
Der Test schlägt fehl mit der Nachricht: "Removing a detached instance".
Das Problem besteht darin, dass die Adresse zuerst über einen EntityManager gezogen wird, aber das Löschen in einem anderen EntityManager, bzw. dessen neuer Transaktion, erfolgen soll. Dadurch ist die Entität detached und muss erst wieder hinzugefügt werden, um sie schließlich löschen zu können:
public static void deleteAdresse(Adresse adresse) {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.remove(em.contains(adresse) ? adresse : em.merge(adresse));
em.getTransaction().commit();
}