Categories
Development Java

Logging

Zuletzt kochte die Log4J Lücke hoch, so dass man sich mit dem Thema Logging auseinander setzen musste.

Mich betraf der Bug nicht besonders, nach eingehender Analyse stellte sich heraus, dass keines meiner im Betrieb befindlichen Projekte Log4J verwendet. Ein Paar Projekte, die ich jahrelang betreuen durfte, waren betroffen, aber für die bin ich nicht mehr verantwortlich und war nur beratend tätig und habe meine Einschätzung und Handlungsempfehlung abgegeben.

Allerdings trägt das grade in der Entwicklung, aber noch nicht in Betrieb gegangene, Projekt Log4J in sich, so dass das Thema vor dem GoLive angegangen werden muss.

Java Logging

Einen sehr schönen, pragmatischen Einstieg in Java Util Logging habe ich auf Java Code Geeks gefunden.

Das einfachste Beispiel, um einen Ausgabe auf der Console zu erhalten:

package deringo.jpa;

import java.util.logging.Logger;

public class TestMain {

	public static void main(String[] args) throws Exception {
		Logger logger = Logger.getLogger(TestMain.class.getName());
		logger.warning("Dies ist nur ein Test!");
		
	}

}

Ein paar Code Beispiele:

package deringo.jpa;

import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TestMain {

	public static void main(String[] args) throws Exception {
		Logger logger = Logger.getLogger(TestMain.class.getName());
		logger.warning("Dies ist nur ein Test!");

		// Warnung wird ausgegeben, Fine nicht
		logger.fine("Eine fine Nachricht. 1");
		logger.warning("Eine warnende Nachricht. 1");
		
		// Das Level des Loggers auf ALL setzen 
		logger.setLevel(Level.ALL);

		// Trotzdem: Warnung wird ausgegeben, Fine nicht
		logger.fine("Eine fine Nachricht. 2");
		logger.warning("Eine warnende Nachricht. 2");

		// Einen Handler für den Logger definieren, der Handler Level wird auf ALL gesetzt
		Handler consoleHandler = new ConsoleHandler();
		consoleHandler.setLevel(Level.ALL);
		logger.addHandler(consoleHandler);

		// Warnung wird ausgegeben, Fine wird ausgegeben
		// ABER: Warnung wird doppelt ausgegeben
		logger.fine("Eine fine Nachricht. 3");
		logger.warning("Eine warnende Nachricht. 3");
	}
}

Überraschend ist erstmal, dass die dritte Ausgabe, zumindest für die Warnung, doppelt erscheint.

Die Erklärung ist, dass es noch einen Root Logger gibt, welcher der Parent des TestMain Loggers ist.
Standardmäßig gibt ein Logger seine Einträge an den Parent Logger weiter. Bzw. an die Handler des Parent Loggers.
Der Root Logger hat die ersten Logs ausgegeben, als der TestMain Logger noch gar keinen Handler hatte, der die Log Einträge verarbeiten konnte.

Wird die Weitergabe an den Parent Handler deaktiviert, wird nicht mehr doppelt geloggt:

package deringo.jpa;

import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;

public class TestMain {

	public static void main(String[] args) throws Exception {
		Logger logger = Logger.getLogger(TestMain.class.getName());

		// Warnung wird ausgegeben, Fine nicht
		logger.fine("Eine fine Nachricht. 1");
		logger.warning("Eine warnende Nachricht. 1");
		
		// Nicht an Parent Handler weiter reichen
		logger.setUseParentHandlers(false);

		// Warnung wird NICHT mehr ausgegeben, Fine ebenfalls nicht
		logger.fine("Eine fine Nachricht. 2");
		logger.warning("Eine warnende Nachricht. 2");

		// Eigenen Handler definieren
		Handler consoleHandler = new ConsoleHandler();
		consoleHandler.setLevel(Level.ALL);
		logger.addHandler(consoleHandler);

		// Warnung wird ausgegeben, Fine wird NICHT ausgegeben
		// Warnung wird NICHT doppelt ausgegeben
		logger.fine("Eine fine Nachricht. 3");
		logger.warning("Eine warnende Nachricht. 3");
		
		// Das Level des Loggers auf ALL setzen 
		logger.setLevel(Level.ALL);

		// Warnung wird ausgegeben, Fine wird ausgegeben
		// Warnung wird NICHT doppelt ausgegeben
		logger.fine("Eine fine Nachricht. 4");
		logger.warning("Eine warnende Nachricht. 4");
	}
}

Java Logging - Konfiguration per Datei

Möchte man die Konfiguration des Java Util Loggers nicht per Code, wie oben, vornehmen, sondern per Datei findet sich ein guter Einstieg auf Wikibooks.

Davon abgeleitet meine Konfigurationsdatei logging.properties, die ich in src/main/resources abgelegt habe:

# Der ConsoleHandler gibt die Nachrichten auf std.err aus
#handlers= java.util.logging.ConsoleHandler

# Alternativ können weitere Handler hinzugenommen werden. Hier z.B. der Filehandler
handlers= java.util.logging.FileHandler, java.util.logging.ConsoleHandler

# Festlegen des Standard Loglevels
.level= INFO
 
############################################################
# Handler specific properties.
# Describes specific configuration info for Handlers.
############################################################

# Die Nachrichten in eine Datei im Benutzerverzeichnis schreiben
java.util.logging.FileHandler.pattern = d:/java%%u.log
java.util.logging.FileHandler.limit = 50000
java.util.logging.FileHandler.count = 1
java.util.logging.FileHandler.formatter = java.util.logging.XMLFormatter

java.util.logging.FileHandler.level = ALL

# Zusätzlich zu den normalen Logleveln kann für jeden Handler noch ein eigener Filter 
# vergeben werden. Das ist nützlich wenn beispielsweise alle Nachrichten auf der Konsole ausgeben werden sollen
# aber nur ab INFO in das Logfile geschrieben werden soll.
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
 
############################################################
# Extraeinstellungen für einzelne Logger
############################################################

# Für einzelne Logger kann ein eigenes Loglevel festgelegt werden.
deringo.jpa.TestMain.level = FINEST

Leider funktionierte es nicht.
Es wird nach wie vor die originale logging.properties von Java genommen, die im Java Installationsverzeichnis $JAVA_HOME/jre unterhalb des lib Verzeichnises liegt, bzw. ab Java 9 in $JAVA_HOME/conf. Vgl. Mkyong

Falls nicht die Original-logging.properties-Datei benutzt werden soll, kann über die System-Property java.util.logging.config.file die stattdessen zu verwendende Datei angegeben werden.

Wie das praktisch geht, kann bei Mkyong nachgesehen werden.

Ich habe folgenden Code verwendet:

package deringo.jpa;

import java.util.logging.Logger;

public class TestMain {

	public static void main(String[] args) throws Exception {
		String path = TestMain.class.getClassLoader().getResource("logging.properties").getFile();
		System.setProperty("java.util.logging.config.file", path);

		Logger logger = Logger.getLogger(TestMain.class.getName());
		
		// Warnung wird ausgegeben, Fine wird ausgegeben
        // Beides auf der Console und in der Datei D:/java0.log
		logger.fine("Eine fine Nachricht. 1");
		logger.warning("Eine warnende Nachricht. 1");
	}
}

Es wird in der Console und der definierten Datei geloggt.

Warum die logging.properties des Projektes nicht standartmäßig anstelle der Java logging.properties gezogen wird, kann ich mir nicht erklären.

SLF4J

SLF4J ist kein Logging Framework, sondern eine Fassade vor der eigentlichen Implementierung. Man kann also im Code mit SLF4J loggen und SLF4J leitet das dann an das gewählte Framework, zB Java Util Logging oder Log4J weiter. So kann man das Logging Framework austauschen ohne den Code anfassen zu müssen.

Ob das jemals jemand vor dem Log4J Bug gemacht hat lasse ich mal dahingestellt, mir gefällt aber das eingebaute Templating, bzw. Parameterisierung, von SLF4J:

Object entry = new SomeObject();
logger.debug("The entry is {}.", entry);

Das SLF4J Manual und das SLF4J Configuration File Example waren mir gute Informationsquellen.

Maven Dependency:

<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>1.7.32</version>
</dependency>

Erster Beispielcode:

package deringo.jpa.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SLF4JTest {

	public static void main(String[] args) {
		Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
		logger.info("Hallo Welt!");
	}

}

Folgende Grafik aus dem SLF4J Manual zeigt, dass nach /dev/null geloggt wurde:

Es wird also eine Logging Framework Implementierung benötigt.

Ich entscheide mich für das Java Util Logging Framework, denn dieses ist in Java bereits enthalten und ich muss keine weitere Bibliothek, wie zB Log4J, in mein Projekt einbinden.

Es kommt also eine weitere Maven Abhängigkeit hinzu:

<properties>
  <org.slf4j.version>1.7.32</org.slf4j.version>
</properties>


<dependencies>
  <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${org.slf4j.version}</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-jdk14 -->
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-jdk14</artifactId>
    <version>${org.slf4j.version}</version>
  </dependency>
</dependencies>

Der selbe Beispielcode von oben:

package deringo.jpa.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SLF4JTest {

	public static void main(String[] args) {
		Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
		logger.info("Hallo Welt!");
	}

}

Führt jetzt zu folgender Ausgabe:

Äquivalent zu dem Code Beispiel zu Java Util Logging - Konfiguration per Datei weiter oben, führt folgender Code zusätzlich zu einem Logging in einer Datei:

package deringo.jpa.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SLF4JTest {

	public static void main(String[] args) {
		String path = SLF4JTest.class.getClassLoader().getResource("logging.properties").getFile();
		System.setProperty("java.util.logging.config.file", path);
		Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
		logger.info("Hallo Welt!");
	}

}

SLF4J & Log4J

Maven:

<properties>
  <org.slf4j.version>1.7.32</org.slf4j.version>
  <log4j.version>2.17.1</log4j.version>
</properties>


<dependencies>
  <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${org.slf4j.version}</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 -->
  <dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${org.slf4j.version}</version>
  </dependency>
  <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
  <dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>${log4j.version}</version>
  </dependency>
</dependencies>

Selber Beispielcode wie oben:

package deringo.jpa.repository;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SLF4JTest {

	public static void main(String[] args) {
		Logger logger = LoggerFactory.getLogger(SLF4JTest.class);
		logger.info("Hallo Welt!");
	}

}

Ausgabe:

Zur Initialisierung bzw. Konfiguration von Log4J wird in src/main/resources eine Datei log4j.properties angelegt:

log4j.debug=false
log4j.rootLogger=INFO, CONSOLE

log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.Encoding=UTF-8
log4j.appender.CONSOLE.layout = org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern = %%d [%%t] %%-5p %%c- %%m%%n

# Configure which loggers log to which appenders
log4j.logger.deringo.jpa=DEBUG

Der Java Code bleibt unverändert, die Ausgabe sieht jetzt aber so aus:

Categories
Development Java

JPA – (nicht) ohne persistence.xml

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.

persistence.xml

Die im vorherigen Post zu JPA sieht folgendermaßen aus:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
  version="2.2">
    <persistence-unit name="myapp-persistence-unit">
        <properties>
            <!-- Configure a database connection in Java SE -->
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://127.0.0.1:5432/myapp" />
            <property name="javax.persistence.jdbc.user" value="postgres" />
            <property name="javax.persistence.jdbc.password" value="PASSWORD" />
 
            <!-- Configure timeouts -->     
            <property name="javax.persistence.lock.timeout" value="100"/>
            <property name="javax.persistence.query.timeout" value="100"/>
        </properties>
    </persistence-unit>
</persistence>

persistence.xml löschen

Der Versuch, die persistence.xml zu löschen führt zu einem Fehler:

persistence.xml minimal

Der einfachste Weg, diesen Fehler zum umgehen, ist eine minimale persistence.xml anzulegen:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
  version="2.2">
    <persistence-unit name="myapp-persistence-unit">
        <properties>
        </properties>
    </persistence-unit>
</persistence>

Wird der EntityManager wie bisher erzeugt, gibt es einen Fehler:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit");

Konfiguration im Java Code

Die ursprüngliche Konfiguration aus der persistence.xml wird jetzt im Java Code vorgenommen:

Properties properties = new Properties();
properties.put("javax.persistence.jdbc.driver", "org.postgresql.Driver");
properties.put("javax.persistence.jdbc.url", "jdbc:postgresql://127.0.0.1:5432/myapp");
properties.put("javax.persistence.jdbc.user", "postgres");
properties.put("javax.persistence.jdbc.password", "PASSWORD");
properties.put("javax.persistence.lock.timeout", "100");
properties.put("javax.persistence.query.timeout", "100");

Der EntityManager wird dann ganz einfach fehlerfrei wie folgt erzeugt:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit", properties);

DB Zugriff testen

Um den fehlerhaften Zugriff mit minimaler persistence.xml zu testen:

// Test 'broken' persistence.xml
Error error = assertThrows(NoClassDefFoundError.class, () -> {
  AdresseRepository.getLastObjectID();
});
assertTrue(error != null);

Um den fehlerlosen Zugriff mit minimaler persistence.xml und Java Konfiguration zu testen:

Properties properties = new Properties();
properties.put("javax.persistence.jdbc.driver", "org.postgresql.Driver");
properties.put("javax.persistence.jdbc.url", "jdbc:postgresql://127.0.0.1:5432/myapp");
properties.put("javax.persistence.jdbc.user", "postgres");
properties.put("javax.persistence.jdbc.password", "PASSWORD");
properties.put("javax.persistence.lock.timeout", "100");
properties.put("javax.persistence.query.timeout", "100");

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit", properties);

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();

assertNotNull(value);

Anderer Error Bugfix

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:

// Test 'broken' persistence.xml
Error error = assertThrows(LinkageError.class, () -> {
  AdresseRepository.getLastObjectID();
});
assertTrue(error != null);

Available Settings

Die Konstanten der verfügbaren Einstellungen sind in der Klasse org.hibernate.cfg.AvailableSettings zu finden.

Dadurch lassen sich die Properties etwas eleganter setzen:

properties.put(org.hibernate.cfg.AvailableSettings.SHOW_SQL, Boolean.TRUE);
// bzw.
properties.put(AvailableSettings.SHOW_SQL, Boolean.TRUE);
// oder
import static org.hibernate.cfg.AvailableSettings.*;
properties.put(SHOW_SQL, Boolean.TRUE);

Umsetzungsvorschlag

Am einfachsten lasse ich meine persistence.xml wie bisher, mit den Einstellungen der Entwicklungsdatenbank.

Einstellungen wie zB der JDBC Driver bleiben auf allen Systemen gleich. Lediglich die URL, Username und das Passwort werden sich ändern.

Diese Werte können als Umgebungsvariable gesetzt werden, zB in einem Docker-File oder im Tomcat, und dann die Werte der persistence.xml überschreiben.

Also wieder die komplette persistence.xml nutzen:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
  version="2.2">
    <persistence-unit name="myapp-persistence-unit">
        <properties>
            <!-- Configure a database connection in Java SE -->
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://127.0.0.1:5432/myapp" />
            <property name="javax.persistence.jdbc.user" value="postgres" />
            <property name="javax.persistence.jdbc.password" value="PASSWORD" />
 
            <!-- Configure timeouts -->     
            <property name="javax.persistence.lock.timeout" value="100"/>
            <property name="javax.persistence.query.timeout" value="100"/>
        </properties>
    </persistence-unit>
</persistence>

Im Java Code könnte man dann die Werte überschreiben, beispielsweise für das Anzeigen der SQLs:

Properties properties = new Properties();
Optional.ofNullable(System.getenv(SHOW_SQL)).ifPresent( value -> properties.put(SHOW_SQL, value));

EntityManagerFactory emf = Persistence.createEntityManagerFactory("myapp-persistence-unit", properties);

Alternativ könnte man auch den Pfad zu einer Konfigurationsdatei auf dem Server setzen und dann von dort die Werte auslesen.

Categories
Database Development Java

JPA

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:

<project xmlns="http://maven.apache.org/POM/4.0.0" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>deringo</groupId>
  <artifactId>jpa</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>JPATest</name>
  <description>JPA Test Project</description>

  <properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>javax.persistence</groupId>
      <artifactId>javax.persistence-api</artifactId>
      <version>2.2</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>5.6.1.Final</version>
    </dependency>
   <!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.2.18</version>
    </dependency>
  </dependencies>

</project>

Verbindungsbeschreibung

Die benötigten Informationen für den Verbindungsaufbau mit der DB werden in der persistence.xml hinterlegt:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
  http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd"
  version="2.2">
    <persistence-unit name="myapp-persistence-unit">
        <properties>
            <!-- Configure a database connection in Java SE -->
            <property name="javax.persistence.jdbc.driver" value="org.postgresql.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:postgresql://127.0.0.1:5432/myapp" />
            <property name="javax.persistence.jdbc.user" value="postgres" />
            <property name="javax.persistence.jdbc.password" value="PASSWORD" />
 
            <!-- Configure timeouts -->     
            <property name="javax.persistence.lock.timeout" value="100"/>
            <property name="javax.persistence.query.timeout" value="100"/>
        </properties>
    </persistence-unit>
</persistence>

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:

    <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
    <dependency>
	  <groupId>org.junit.jupiter</groupId>
	  <artifactId>junit-jupiter-api</artifactId>
	  <version>5.8.1</version>
	  <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.hamcrest/hamcrest -->
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest</artifactId>
      <version>2.2</version>
      <scope>test</scope>
    </dependency>

package deringo.jpa.repository;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.junit.jupiter.api.Test;

import deringo.jpa.entity.Adresse;

public class AdresseRepositoryTest {

	@Test
	public void getAnmeldungById() {
		int adresseID = 1;
		Adresse adresse = AdresseRepository.getAdresseById(adresseID);
		assertNull(adresse);
		
		adresseID = 4;
		adresse = AdresseRepository.getAdresseById(adresseID);
		assertNotNull(adresse);
		assertThat(adresse.getObjectID(), is(adresseID));
		assertThat(adresse.getStrasse(), is("Beispielstrasse"));
		assertThat(adresse.getOrt(), is("Beispielstadt"));
	}
}

Projektstruktur

Query

Alle Adressen eines Ortes suchen:

	public static List<Adresse> getAdresseByOrt(String ort) {
		EntityManager em = emf.createEntityManager();
		TypedQuery<Adresse> 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:

	public static Adresse getAdresseByPersonID(int personId) {
		String sql = "SELECT adresse_object_id FROM adresse_person WHERE person_object_id = " + personId;
		EntityManager em = emf.createEntityManager();
		Integer adresseId;
		try {
			adresseId = (Integer)em.createNativeQuery(sql).getSingleResult();
		} catch (NoResultException nre) {
			return null;
		}
		return getAdresseById(adresseId.intValue());
	}

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<Person> personen = new ArrayList<>();
  [...]
}

Anschließend noch die Getter&Setter, toString, hashCode&equals neu generieren und einen Test ausführen:

	@Test
	public void getAnmeldungById() {
		int adresseID = 4;
		adresse = AdresseRepository.getAdresseById(adresseID);
		assertNotNull(adresse);
		assertThat(adresse.getObjectID(), is(adresseID));
		assertThat(adresse.getStrasse(), is("Beispielstrasse"));
		assertThat(adresse.getOrt(), is("Beispielstadt"));		
		assertThat(adresse.getPersonen().size(), is(3));
	}

Der Test funktioniert.

ABER: Folgende Zeile am Ende bewirkt einen StackOverflow Error:

	public void getAnmeldungById() {
        [...]
		System.out.println(adresse);
    }

Das Problem ist die generierte toString-Methode in Person:

	@Override
	public String toString() {
		return String.format("Person [objectID=%%s, vorname=%%s, nachname=%%s, adresse=%%s]", objectID, vorname, nachname, 
                             adresse);
	}

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:

	@Override
	public String toString() {
		return String.format("Person [objectID=%%s, vorname=%%s, nachname=%%s, adresse=%%s]", objectID, vorname, nachname, 
                             adresse == null ? null : adresse.getObjectID());
	}

siehe auch: https://stackoverflow.com/questions/23973347/jpa-java-lang-stackoverflowerror-on-adding-tostring-method-in-entity-classes

Neuen Eintrag speichern

Adresse speichern:

	public static void saveAdresse(Adresse adresse) {
		EntityManager em = emf.createEntityManager();
		em.getTransaction().begin();
		if (adresse.getObjectID() == 0) {
			em.persist(adresse);
		} else {
			em.merge(adresse);
		}
		em.getTransaction().commit();
	}

Testen:

	@Test
	public void saveNewAdresse() {
		int objectID = AdresseRepository.getLastObjectID();

		Adresse adresse = new Adresse();
		adresse.setStrasse("neue Stasse");
		adresse.setOrt("neuer Ort");
		assertThat(adresse.getObjectID(), is(0));
		AdresseRepository.saveAdresse(adresse);
		assertThat(adresse.getObjectID(), is(objectID + 1));
		assertThat(adresse.getOrt(), is("neuer Ort"));
		
		adresse.setOrt("neuerer Ort");
		AdresseRepository.saveAdresse(adresse);
		assertThat(adresse.getObjectID(), is(objectID + 1));
		assertThat(adresse.getOrt(), is("neuerer Ort"));
	}

Eintrag löschen

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();
	}
Categories
Database Development

PostgreSQL Docker-Compose Setup

Im vorletzten Post: PostgreSQL hatte ich beschrieben, wie ich zwei Bestandsdatenbanken analysiert und in eine PostgreSQL-DB in einem Docker Container gebracht habe, samt PGAdmin.

Jetzt möchte ich einen Schritt weiter gehen und das komplette Setup über ein Script starten können: DB, PGAdmin & SQL-Scripte. Dazu verwende ich Docker-Compose.

Ausgangslage

In PostgreSQL hatte ich bereits die zugrundeliegenden Docker-Images ermittelt: postgres:13.4-buster für die DB und dpage/pgadmin4 für PGAdmin. Inzwischen gibt es aber ein aktuelleres Image für die DB, das ich verwenden werde: postgres:13.5-bullseye

docker pull postgres:13.5-bullseye
docker pull dpage/pgadmin4

Für die SQL-Daten werde ich auf den Artikel PostgreSQL IDs zurückgreifen und daraus zwei Scripte machen, eines für das Schema der DB und eines mit den "Masterdaten" mit denen das Schema initial befüllt werden soll.

CREATE SEQUENCE object_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

CREATE TABLE person (
    object_id integer NOT NULL DEFAULT nextval('object_id_seq'::regclass),
    vorname varchar(255),
    nachname varchar(255),
    CONSTRAINT person_pkey PRIMARY KEY (object_id)
);

CREATE TABLE adresse (
    object_id integer NOT NULL DEFAULT nextval('object_id_seq'::regclass),
    strasse varchar(255),
    ort varchar(255),
    CONSTRAINT adresse_pkey PRIMARY KEY (object_id)
);
INSERT INTO person (vorname, nachname) VALUES ('Max', 'Mustermann');
INSERT INTO person (vorname, nachname) VALUES ('Peter', 'Person');
INSERT INTO person (vorname, nachname) VALUES ('Donald', 'Demo');

INSERT INTO adresse (strasse, ort) VALUES ('Beispielstrasse', 'Beispielstadt');
INSERT INTO adresse (strasse, ort) VALUES ('Erpelweg', 'Entenhausen');
INSERT INTO adresse (strasse, ort) VALUES ('Bruchstrasse', 'Berlin');

Auf der Seite von Docker Compose bringe ich in Erfahrung, dass die aktuelle Version von Docker Compose 3.9 ist und erstelle schon mal die Datei:

version: "3.9"  # optional since v1.27.0

Ordernstrucktur:

Images starten

Bisher habe ich PostgreSQL und PGAdmin über folgende Kommandos gestartet:

docker run --name myapp-db -p 5432:5432 -e POSTGRES_PASSWORD=PASSWORD -d postgres:13.4-buster
docker run --name myapp-pgadmin -p 80:80 -e PGADMIN_DEFAULT_EMAIL=admin@admin.com -e PGADMIN_DEFAULT_PASSWORD=admin -d dpage/pgadmin4

Diese Kommandos werden in ein Docker-Compose Script transferiert:

version: "3.9"  # optional since v1.27.0
services:
  myapp-db:
    image: postgres:13.5-bullseye
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_PASSWORD=PASSWORD
  myapp-pgadmin:
    image: dpage/pgadmin4
    ports:
      - "80:80"
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@admin.com
      - PGADMIN_DEFAULT_PASSWORD=admin

Jetzt können beide Images mit einem einfachen Befehl gestartet werden:

\myapp> docker-compose up
Creating network "myapp_default" with the default driver
Creating myapp_myapp-db_1      ... done
Creating myapp_myapp-pgadmin_1 ... done
Attaching to myapp_myapp-db_1, myapp_myapp-pgadmin_1

PGAdmin im Browser starten: http://localhost:80
Login wie bisher mit admin@admin.com / admin

Im nächsten Schritt gibt es bereits eine entscheidende Änderung:
Während bisher beide Container isoliert nebeneinander liefen und nur über den Host-Rechner kommunizieren konnten, wurde durch Docker-Compose automatisch beim Start ein Netzwerk ("myapp_default") angelegt, in dem beide Container laufen. Außerdem sind beide Container über ihren Servicenamen ("myapp-db" & "myapp-pgadmin") erreichbar.

Dadurch muss nicht mehr die IP des Host-Rechners ermittelt werden (die sich manchmal ändert), sondern es kann der Name genommen werden:

Datenbank erstellen

In der PostgreSQL Instanz muss jetzt eine Datenbank erzeugt werden, in der die Anwendungsdaten gespeichert werden.

Hierzu gehen wir in den DB Container.
Allerdings ist der Name anders als bisher: Es wurde der Verzeichnisname als Präfix davor und eine 1 (für die 1. und in unserem Fall einzige Instanz) als Postfix dahinter gehangen und so lautet der Name : myapp_myapp-db_1

docker exec -it myapp_myapp-db_1 bash

Im Container erzeugen wir die DB:

su postgres
createdb myappdb
exit

So war es zumindest bisher, einfacher geht es mit Docker-Compose und dem Setzten der Environment-Variablen POSTGRES_DB wodurch die DB automatisch angelegt und verwendet wird.
Sicherlich hätte ich das auch bisher im Docker Kommando so nehmen können, aber im letzten Post hatte ich es zum einen mit zwei DBs zu tun und zum anderen musste ich eh auf die Kommandozeile um die DBs einzuspielen.

version: "3.9"
services:
  myapp-db:
    image: postgres:13.5-bullseye
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_PASSWORD=PASSWORD
      - POSTGRES_DB=myappdb
  myapp-pgadmin:
    image: dpage/pgadmin4
    ports:
      - "80:80"
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@admin.com
      - PGADMIN_DEFAULT_PASSWORD=admin

Datenbank befüllen

Bisher war der Weg, die SQL-Dateien in den Container zu kopieren und von dort einzuspielen:

# Dateien in Container kopieren
docker cp database/Masterdata.sql myapp_myapp-db_1:/tmp
docker cp database/Schema.sql myapp_myapp-db_1:/tmp
# In Container wechseln
docker exec -it myapp_myapp-db_1 bash

Im Container SQLs einspielen:

su postgres
psql myappdb < /tmp/Schema.sql
psql myappdb < /tmp/Masterdata.sql
exit

Einfacher geht es über Docker-Compose und den Mechanismus, dass PostgreSQL automatisch die Dateien importiert, die im Verzeichnis /docker-entrypoint-initdb.d/ liegen. Und zwar in alphabetischer Reihenfolge.

version: "3.9"
services:
  myapp-db:
    image: postgres:13.5-bullseye
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_PASSWORD=PASSWORD
      - POSTGRES_DB=myappdb
    volumes:
      - ./database/Schema.sql:/docker-entrypoint-initdb.d/1-Schema.sql
      - ./database/Masterdata.sql:/docker-entrypoint-initdb.d/2-Masterdata.sql
  myapp-pgadmin:
    image: dpage/pgadmin4
    ports:
      - "80:80"
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@admin.com
      - PGADMIN_DEFAULT_PASSWORD=admin

PGAdmin Einstellungen persistieren

Während der Entwicklung öfters mal die DB platt macht und komplett neu aufsetzt: Mit Docker ist das schnell gemacht. Mit Docker-Compose sind es jetzt nur noch zwei Befehle:

# stop and remove stopped containers
docker-compose down
# start containers
docker-compose up

Einen Nachteil gibt es allerdings: Die Einstellungen im PGAdmin gehen ebenfalls flöten und müssen neu eingegeben werden.
Die Lösung: Der PGAdmin Container bekommt ein persistentes Volume, das ein docker-compose down übersteht. Und wenn es doch mal neu aufgesetzt werden muss, ist das einfach über das -v Flag umsetzbar: docker-compose down -v

version: "3.9"
services:
  myapp-db:
    image: postgres:13.5-bullseye
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_PASSWORD=PASSWORD
      - POSTGRES_DB=myappdb
    volumes:
      - ./database/Schema.sql:/docker-entrypoint-initdb.d/1-Schema.sql
      - ./database/Masterdata.sql:/docker-entrypoint-initdb.d/2-Masterdata.sql
  myapp-pgadmin:
    image: dpage/pgadmin4
    ports:
      - "80:80"
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@admin.com
      - PGADMIN_DEFAULT_PASSWORD=admin
    volumes:
      - pgadminvolume:/var/lib/pgadmin
volumes:
  pgadminvolume: {}

DB Image mit Schema und Masterdata

An Schema und Stammdaten wird sich erstmal nichts ändern. Daher wäre es gut, wenn beim Neubau der Container die DB bereits mit Schema und Stammdaten gestartet wird und nicht diese erst aufbauen muss.

Das wird dadurch erreicht, dass ein Image gebaut wird, dass die PostgreSQL DB sowie Schema und Stammdaten enthält.

Im Verzeichnis database wird eine Datei Dockerfile angelegt. Dieses Dockerfile enthält die Informationen zum Bau des DB Images.

FROM postgres:13.5-bullseye
 ENV POSTGRES_PASSWORD PASSWORD
 ENV POSTGRES_DB myappdb
COPY ./Schema.sql /docker-entrypoint-initdb.d/1-Schema.sql
COPY ./Masterdata.sql /docker-entrypoint-initdb.d/2-Masterdata.sql
version: "3.9"
services:
  myapp-db:
    build: ./database
    ports:
      - "5432:5432"
  myapp-pgadmin:
    image: dpage/pgadmin4
    ports:
      - "80:80"
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@admin.com
      - PGADMIN_DEFAULT_PASSWORD=admin
    volumes:
      - pgadminvolume:/var/lib/pgadmin
volumes:
  pgadminvolume: {}

Gestartet wird wie gewohnt:

docker-compose up

Sollte es erforderlich sein, das Image neu zu bauen, zB wenn ich das Schema verändert hat:

docker-compose up --build
# or
docker-compose build

UPDATE: Das war leider nix mit dem vorgefüllten Image

Das Dockerfile enthält zwar den Schritt die SQL Dateien in das Image zu kopieren. Es fehlt aber der Schritt, bei dem diese Dateien in die Datenbank hineinmigriert werden. Das geschieht wie zuvor auch erst beim Starten des Containers. Mein Ziel, einen Image zu haben, dass diese Daten bereits enthält, habe ich damit also leider nicht erreicht. Das Dockerfile enthält keine Informationen, die nicht zuvor auch schon im Docker Compose enthalten waren.

Da mich das herumkaspern mit diesem Problem heute den ganzen Tag gekostet und abgesehen vom Erkenntnisgewinn leider nichts gebracht hat, kehre ich zur Docker Compose Variante zurück, bei der die DB beim Starten des Containers gebaut wird. Für das Beispiel auf dieser Seite macht das praktisch gesehen keinen Unterschied, da die Scripte winzig sind. Für mein Projekt leider schon, da benötigt der Aufbau der DB ein paar Minuten. Für die Anwendungsentwicklung, bei der ich die DB alle paar Tage mal neu aufsetze, ist das durchaus OK. Für Tests, die mit einer frischen DB starten sollen, die am Ende weggeschmissen wird, ist das schon ein Problem.

Also Rückkehr zu Docker Compose, diesmal mit einem extra Volume für die DB. Das kann ich dann gezielt löschen.

version: "3.9"
services:
  myapp-db:
    image: postgres:13.5-bullseye
    ports:
      - "5432:5432"
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=PASSWORD
      - POSTGRES_DB=myapp
    volumes:
      - postgresvolume:/var/lib/postgresql/data
      - ./database/Schema.sql:/docker-entrypoint-initdb.d/1-Schema.sql
      - ./database/Masterdata.sql:/docker-entrypoint-initdb.d/2-Masterdata.sql
  myapp-pgadmin:
    image: dpage/pgadmin4
    ports:
      - "80:80"
    environment:
      - PGADMIN_DEFAULT_EMAIL=admin@admin.com
      - PGADMIN_DEFAULT_PASSWORD=admin
    volumes:
      - pgadminvolume:/var/lib/pgadmin
volumes:
  postgresvolume: {}
  pgadminvolume: {}

Docker Befehle

# Create and start containers
docker-compose up
# Image neu bauen
docker-compose up --build
# or
docker-compose build

# Stop containers
docker-compose stop
# Start containers
docker-compose start

# Stop containers and remove them
docker-compose down
# Stop containers, remove them and remove volumes
docker-compose down -v

Categories
Database Development

PostgreSQL IDs

Angenommen, wir haben eine Tabelle mit Personen:

CREATE TABLE person (
	vorname varchar(255),
    nachname varchar(255)
);

Diese Personen sollen alle eine eindeutige ID bekommen und diese soll automatisch beim Einfügen generiert werden.

Sequenz

Dazu kann man eine Sequenz anlegen und aus dieser die ID befüllen:

CREATE SEQUENCE person_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

CREATE TABLE person (
    id integer NOT NULL DEFAULT nextval('person_id_seq'::regclass),
	vorname varchar(255),
    nachname varchar(255),
    CONSTRAINT person_pkey PRIMARY KEY (id)
);

Anschließend ein paar Personen hinzufügen:

INSERT INTO person (vorname, nachname) VALUES ('Max', 'Mustermann');
INSERT INTO person (vorname, nachname) VALUES ('Peter', 'Person');
INSERT INTO person (vorname, nachname) VALUES ('Donald', 'Demo');

Und anzeigen lassen:

SELECT * FROM person;

Identity

Da es etwas lästig ist, immer für jede Tabelle jeweils eine eigene Sequenz anlegen und mit der ID verknüpfen zu müssen, wurde die Frage an mich herangetragen, ob es da nicht soetwas wie autoincrement gäbe, wie man es von MySQL kennen würde.

Nach kurze Recherche fand sich, dass es soetwas natürlich auch für PostgreSQL gibt und zwar seit der Version 10 als "IDENTITY".

CREATE TABLE adresse (
    id int GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
	strasse varchar(255),
    ort varchar(255)
);

Anschließend ein paar Adressen hinzufügen:

INSERT INTO adresse (strasse, ort) VALUES ('Beispielstrasse', 'Beispielstadt');
INSERT INTO adresse (strasse, ort) VALUES ('Erpelweg', 'Entenhausen');
INSERT INTO adresse (strasse, ort) VALUES ('Bruchstrasse', 'Berlin');

Und anzeigen lassen:

SELECT * FROM adresse;

Der Vorteil der Identity, was man so auf den ersten Blick sieht, ist also, dass man sich etwas stumpfe Tipparbeit spart und keine Sequenz anlegen, mit der ID verknüpfen und die ID als Primary Key definieren muss.

Eine Betrachtung der Unterschiede zwischen SEQUENCE und IDENTITY habe ich leider nicht finden können.

Vermutlich gibt es da keine großen technischen Unterschiede, die IDENTITY scheint mir eine anonyme SEQUENCE zu sein.

Object ID Sequenz

Die IDENTITY kann man nur für einen TABLE nutzen, die SEQUENCE könnte man für mehrere Tabellen nutzen und so eine datenbankweite eindeutige ID verwenden.

Beispielsweise eine eindeutige Object ID Sequenz anlegen und für die Tabellen Person und Adresse verwenden:

CREATE SEQUENCE object_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1;

CREATE TABLE person (
    object_id integer NOT NULL DEFAULT nextval('object_id_seq'::regclass),
	vorname varchar(255),
    nachname varchar(255),
    CONSTRAINT person_pkey PRIMARY KEY (object_id)
);

CREATE TABLE adresse (
    object_id integer NOT NULL DEFAULT nextval('object_id_seq'::regclass),
	strasse varchar(255),
    ort varchar(255),
    CONSTRAINT adresse_pkey PRIMARY KEY (object_id)
);

Anschließend ein paar Personen und Adressen hinzufügen:

INSERT INTO person (vorname, nachname) VALUES ('Max', 'Mustermann');
INSERT INTO person (vorname, nachname) VALUES ('Peter', 'Person');
INSERT INTO person (vorname, nachname) VALUES ('Donald', 'Demo');

INSERT INTO adresse (strasse, ort) VALUES ('Beispielstrasse', 'Beispielstadt');
INSERT INTO adresse (strasse, ort) VALUES ('Erpelweg', 'Entenhausen');
INSERT INTO adresse (strasse, ort) VALUES ('Bruchstrasse', 'Berlin');

Und anzeigen lassen:

SELECT * FROM person;
SELECT * FROM adresse;

Wie man sehen kann, wurde die ID fortlaufend über beide Tabellen vergeben. Dadurch erhält man eine datenbankweit eindeutige, fortlaufende ID.

UUID

Und weil ich grade schon dabei bin: Seit Version 13 bringt PostgreSQL auch standartmäßig die Möglichkeit einer UUID mit.

Eine UUID ist ein Universally Unique Identifier.
Manchmal, typischerweise in Zusammenhang mit Microsoft, wird auch der Ausdruck GUID Globally Unique Identifier verwendet.

SELECT * FROM gen_random_uuid ();

UUIDs sind ebenfalls datenbankweit (und darüber hinaus) eindeutig. Allerdings sind die IDs nicht mehr fortlaufend.

UUIDs sind etwas langsamer als Sequenzen und verbrauchen etwas mehr Speicher.

Das Beispiel von vorhin:

CREATE TABLE person (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
	vorname varchar(255),
    nachname varchar(255)
);

CREATE TABLE adresse (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
	strasse varchar(255),
    ort varchar(255)
);

Anschließend ein paar Personen und Adressen hinzufügen:

INSERT INTO person (vorname, nachname) VALUES ('Max', 'Mustermann');
INSERT INTO person (vorname, nachname) VALUES ('Peter', 'Person');
INSERT INTO person (vorname, nachname) VALUES ('Donald', 'Demo');

INSERT INTO adresse (strasse, ort) VALUES ('Beispielstrasse', 'Beispielstadt');
INSERT INTO adresse (strasse, ort) VALUES ('Erpelweg', 'Entenhausen');
INSERT INTO adresse (strasse, ort) VALUES ('Bruchstrasse', 'Berlin');

Und anzeigen lassen:

SELECT * FROM person;
SELECT * FROM adresse;

Categories
Database Development

PostgreSQL

Für die Neu- und Weiterentwicklung einer Anwendung habe ich zur Analyse die Bestandsanwendung samt Datenbanken bekommen.
Für die Analyse musste ich zunächst die Datenbanken zum laufen bekommen und uA mit einem DB-Client einsehen.

Ich habe zum einen eine Datei dump.backup bekommen. Zunächst musste ich herausfinden, um was für eine Datei es sich dabei handelt, dazu nutzte ich das Linux Tool file:

apt update
apt install file
file /tmp/dump.backup
# /tmp/dump.backup: PostgreSQL custom database dump - v1.14-0

Es handelt sich also um einen Dump einer PostgreSQL Datenbank. Und im Dump konnte ich eine Versionsnummer 13.0 finden.

Die andere Datei mydb.sql.gz beinhaltet eine gezippte Version eines SQL Exports einer PostgreSQL DB Version 13.2 von einem Debian 13.2 Server.

Im Laufe der weiteren Analyse der DB Exporte stellte sich heraus, dass der dump.backup die PostGIS Erweiterung der PostgreSQL DB benötigt, welche mit installiert werden muss.

PostgreSQL Datenbank Docker Image

Ich werde beide Datenbanken in einer Docker Version installieren, dazu werde ich eine DB Instanz starten, in der beide DBs installiert werden.

Die Docker Seite für Postgres: Postgres - Official Image | Docker Hub

Da es sich um die Versionsnummer 13.0 und 13.2 handelt, werde ich ein aktuelles Image von Version 13 verwenden, was zum Projektzeitpunkt Version 13.4 war.

Da zumindest eine der beiden DBs auf einem Debian System gehostet ist, werde ich ein Debian Image wählen.

Die Wahl des Images fällt also auf: postgres:13.4-buster

Datenbank installieren

Zuerst das Docker Image ziehen:

docker pull postgres:13.4-buster

Datenbank starten:

docker run --name myapp-db -p 5432:5432 -e POSTGRES_PASSWORD=PASSWORD -d postgres:13.4-buster

DB-Dateien in den laufenden Docker Container kopieren:

docker cp mydb.sql.gz myapp-db:/tmp
docker cp dump.backup myapp-db:/tmp

In den laufenden Container wechseln:

docker exec -it myapp-db bash

PostGIS installieren:

apt update
apt install -y postgresql-13-postgis-3-scripts

DB von Dump erstellen:

su postgres
createdb mydb_dump
pg_restore -d mydb_dump -v /tmp/dump.backup
exit

DB von SQL erstellen:

su postgres
cd /tmp
gunzip mydb.sql.gz
createdb mydb_sql
pg_restore -d mydb_sql -v /tmp/mydb.sql
exit

Die Datenbanken sind installiert und es kann mittels eines letzten exit der Container verlassen werden.

DB Client PGAdmin installieren

Um in die Datenbanken hinein sehen zu können wird ein Client Programm benötigt. Sicherlich es gibt da bereits etwas auf der CommandLine Ebene:

su postgres
psql mydb_sql

Übersichtlich oder komfortabel ist das aber nicht. Daher möchte ich ein Tool mit einer grafischen Oberfläche verwenden. Die Wahl fiel auf pgAdmin, welches sich leicht von einem Docker Image installieren und anschließend über den Browser bedienen lässt.

Zuerst das Docker Image ziehen:

docker pull dpage/pgadmin4

pgAdmin parametrisiert starten:

docker run --name myapp-pgadmin -p 80:80 -e PGADMIN_DEFAULT_EMAIL=admin@admin.com -e PGADMIN_DEFAULT_PASSWORD=admin -d dpage/pgadmin4

Login:
* Email: admin@admin.com
* Passwort: admin

Port: 80

Der pgAdmin ist über den Browser aufrufbar: http://localhost/

Für die Konfiguration für die Verbindung zur zuvor gestarteten PostgreSQL Datenbank benötige ich die IP meines Rechners, die ich mittels ipconfig herausfinden kann.

Categories
Development Java

Tomcat Start beschleunigen

Das Starten des Tomcat-Servers hat für ein Projekt sehr lange gedauert. Im Eclipse kann man die Zeit bis zum Timeout hoch setzen, den Nerven des Entwicklers hilft das aber nur bedingt.

Eine Ursache für die lange Startzeit liegt darin, dass der Tomcat-Server beim Start alle jar-Files nach Taglibs durchsucht. Das Projekt hat sehr viele Libraries.

Eine Abhilfe schafft hier die Konfiguration, dass Tomcat keine jar-Files scannen soll, außer denen, in denen eine Taglib enthalten ist.

Wie man Jars mit Taglibs findet

Tomcat kann anzeigen lassen, welche Jars, die beim Start gescannt werden, Taglibs enthalten. Dazu muss das entsprechende Log-Level gesetzt werden.

In meinem Fall musste ich lediglich die logging.properties aus dem Original-Tomcat Verzeichnis in das Verzeichnis des Eclipse Tomcats kopieren:

Am Ende der logging.properties das Log-Level für den TLDScanner setzen:

[...]

org.apache.jasper.compiler.TldLocationsCache.level = FINE
org.apache.jasper.servlet.TldScanner.level = FINE

In den VM Arguments des Tomcats muss der Pfad zur logging.properties angegeben werden:

Beim Start wird jetzt angezeigt, in welchen JARs TLDs zu gefunden wurden.

Wie nur noch ausgewählte JARs gescannt werden

In aktuellen Projekt wurden folgende JARs mit TLDs gefunden:

  • standard-1.1.2.jar
  • jstl-1.2.jar
  • jsf-impl-2.2.20.jar
  • tomahawk20-1.1.14.jar

Die Konfiguration des JARScanFilters für den Tomcat Server erfolgt in der catalina.properties Datei.

Bei den jarsToSkip lasse ich alle (*.jar) skippen.

Bei den jarsToScan füge ich obige JARs hinzu:

Alleine durch diese Konfigurationsänderung konnte die Startzeit von 25 Sekunden auf 10 Sekunden reduziert werden.

JarScanner Konfiguration im Projekt

Der obige Weg beschreibt die Konfigurationsänderung im Server. Das hat den Nachteil, dass jede Serverinstanz diese Konfiguration gesetzt bekommen muss. Eine Konfiguration im Projekt selbst hat den Vorteil, dass zB alle Mitentwickler direkt mit profitieren können und nicht erst die Konfiguration selbst setzen müssen.

Die Konfiguration im Projekt erfolgt über context.xml:

<?xml version="1.0" encoding="UTF-8"?>
<Context>
  <JarScanner>
    <JarScanFilter tldScan="standard-1.1.2.jar,jstl-1.2.jar,jsf-impl-2.2.20.jar,tomahawk20-1.1.14.jar" 
                   defaultTldScan="false" 
                   defaultPluggabilityScan="false"/>
  </JarScanner>
</Context>