Prof. Dr.-Ing. Oliver Radfelder
Informatik / Wirtschaftsinformatik
Hochschule Bremerhaven

Kurz und Knapp: postgresql

In den Docker-Containern ist das Datenbank-Management System postgresql installiert. Anders aber als bei der zentralen Instanz der mariadb musst Du Dich selbst um das Verwalten (Datenbank anlegen, User anlegen, Hochfahren, Herunterfahren, ...) kümmern.

Aktiviere den Datenbankserver innerhalb des Docker-Containers mit:

sudo /etc/init.d/postgresql start

Du kannst diese Zeile in das Skript atreboot.sh in dem Home-Verzeichnis des Docker-Containers aufnehmen, das bei jedem hbv_dockeraktivieren aufgerufen wird. Passend dazu sollte dann auch in atstop.sh

sudo /etc/init.d/postgresql stop

stehen.

Lege als User postgres eine Datenbank und einen User an und gib dem User die Rechte für das Schema public in der Datenbank:

sudo -u postgres psql -c "create database mypgdb"
sudo -u postgres psql -c "create user mypguser with password 'einsicherespasswort'"
sudo -u postgres psql -d mypgdb -c "grant all privileges on schema public to mypguser"

Stelle sicher, dass Du tatsächlich ein sicheres Passwort wählst!

Nun kannst Du Dich als User mypguser bei dem Server anmelden:

PGPASSWORD=einsicherespasswort psql -h localhost mypgdb mypguser

Trage die Zeile:

localhost:5432:mypgdb:mypguser:einsicherespasswort

in die Datei .pgpass in dem Homeverzeichnis des Docker-Containers ein und ändere die Zugriffsrechte so, dass nur Du Zugriff darauf hast. Dann kannst Du Dir das Setzen der Variable PGPASSWORD sparen.

Wenn Du zudem noch die Umgebungsvariablen PGHOST, PGDATABASE und PGUSER in Deiner .bashrc setzt, brauchst Du nur noch psql ohne Argumente aufzurufen und bist mit Deiner Datenbank verbunden.

Nun kannst Du mit SQL Tabellen erzeugen:

create table demo (id serial primary key not null,name text);
insert into demo (name) values ('moin');

Der Jdbc-Treiber für Postgresql ist in dem tomcat in dem Container bereits enthalten. Du musst in der Web-Anwendung lediglich dafür sorgen, dass in der Datei META-INF/context.xml ein Verweis hinterlegt wird, über den dann im Servlet darauf zugreifen wird.

<Context>
  <Resource name="jdbc/mypgdb"
    auth="Container"
    type="javax.sql.DataSource"
    initialSize="3"
    maxTotal="40"
    maxWaitMillis="5000"
    username="mypguser"
    password="einsicherespasswort"
    driverClassName="org.postgresql.Driver"
    url="jdbc:postgresql://localhost:5432/mypgdb"
 />
</Context>

In dem Javacode sieht es dann wie gewohnt aus:

    Context initCtx = new InitialContext();
    Context envCtx = (Context) initCtx.lookup("java:/comp/env");
    DataSource ds = (DataSource)envCtx.lookup("jdbc/mypgdb");
    Connection connection = ds.getConnection();

In php wiederum sieht es wie folgt aus:

<?php
$dbconn = pg_connect("host=localhost dbname=mypgdb user=mypguser password=einsicherespasswort")
    or die('Verbindungsaufbau fehlgeschlagen: ' . pg_last_error());
$query = 'SELECT * FROM demo';
$result = pg_query($query) 
  or die('Abfrage fehlgeschlagen: ' . pg_last_error());
while ($line = pg_fetch_array($result, null, PGSQL_ASSOC)) {
    foreach ($line as $col_value) {
        echo "$col_value ";
    }
    echo "\n";
}

pg_free_result($result);

pg_close($dbconn);
      

In den Docker-Containern ist bereits alles vorbereitet, um mit php auf postgresql zuzugreifen. In den VMs in Ubuntu müsst Ihr noch mit apt das Paket php-pgsql installieren und den pdo-Treiber mit sudo phpenmod pdo_pgsql aktivieren. Vergesst dabei nicht, den Datenbankserver neu zu starten.

Oder eher mit PDO:

<?php
$sql = 'SELECT * from demo';
$conn = new PDO("pgsql:host=localhost dbname=mypgdb", 'mypguser', 'einsicherespasswort');
$result = $conn->query($sql);
while($row = $result->fetch(PDO::FETCH_ASSOC)) {
  echo $row['id']. ' ' . $row['msg'] . "\n";
}
      

Wenn Du ein wenig SQL üben willst, lade den Inhalt der dvdrentals-Datenbank in PostgreSQL. In /home/common/data/ auf hopper ist die Datei dvdrentals-postgres.tar

scp /home/common/data/dvdrental-postgres.tar mydocker:
ssh mydocker
sudo -u postgres psql -c 'create database dvdrental;'
sudo -u postgres pg_restore -d dvdrental dvdrental-postgres.tar
sudo -u postgres psql dvdrental
dvdrental=# 
select * from actor where last_name = 'Akroyd';
┌──────────┬────────────┬───────────┬────────────────────────┐
│ actor_id │ first_name │ last_name │      last_update       │
├──────────┼────────────┼───────────┼────────────────────────┤
│       58 │ Christian  │ Akroyd    │ 2013-05-26 14:47:57.62 │
│       92 │ Kirsten    │ Akroyd    │ 2013-05-26 14:47:57.62 │
│      182 │ Debbie     │ Akroyd    │ 2013-05-26 14:47:57.62 │
└──────────┴────────────┴───────────┴────────────────────────┘

      

Wenn wir Daten exportieren wollen, dann können wir mit dem Schalter --csv arbeiten, aber sauberer scheint es mit copy zu werden:

psql -c "\copy (select * from demo where name like 'a%') to STDOUT delimiter ';' csv header;"
psql -c "\copy (select * from demo d, demo e where d.name like 'a%') to STDOUT WITH (HEADER TRUE,FORMAT csv,FORCE_QUOTE *);"
      

Alternativ reicht der Schalter --csv, dann lässt sich der Delimiter mit -P festlegen:

psql -P csv_fieldsep=';' --csv -c "select * from demo where name like 'a%'"
      

Besonders bekannt ist PostgreSQL für die Erweiterung postgis - eine Geodatenbank-Erweiterung. Die zugehörigen Module sind in dem Docker-Container bereits installiert, sie müssen lediglich für die jeweilige Datenbank akiviert werden.

sudo -u postgres psql -d mypgdb
mypgdb=# create extension postgis;
      

Weiteres findet sich in der Dokumentation zu postgis. Andere interessante Erweiterungen sind file_fdw (ein schönes Beispiel unter rustprooflabs), pgcrypto, plperl, http, pltcl, plpython3u oder postgres_fdw, die alle installiert sind. Und wer ein bisschen mehr Buzzwords braucht: Wie wäre es mit PostgreSQL und Machine Learning?

Der Postgres-Server ist nur von innerhalb des Docker-Containers zu erreichen. Solltest Du ihn von außen ansprechen wollen, heisst es zu recherchieren und man-Pages zu lesen. Du musst dann den Port des Servers auf 6000 umbiegen (/etc/postgresql/17/main/postgresql.conf), dafür sorgen, dass der Server auf allen Interfaces lauscht (gleiche Datei) und in /etc/postgresql/.../main/pg_hba.conf noch einstellen, wie die Authentifizierung funktionieren soll (host all all 0.0.0.0/0 scram-sha-256). Das alles kannst Du als User postgres durchführen. Allerdings sind all diese Einstellungen nach dem Aktivieren mit hbv_dockeraktivieren wieder verschwunden. Also hilft es nichts: da musst Du dann mit sed ran und diese Dateien beim Hochfahren in atreboot.sh umschreiben vor dem Starten der Datenbank. Alternativ kannst Du auch die Konfigurationsdateien in Dein Homeverzeichnis kopieren, editieren und dann beim Hochfahren in atreboot.sh an die entsprechende Stelle schieben.

In PostgreSQL programmiert man stored procedures mit plpgsql - einer recht mächtigen Sprache, die an Oracles plsql angelehnt ist.

CREATE OR REPLACE FUNCTION sum_of_two_numbers(m integer, n integer) 
RETURNS integer AS $$
        BEGIN
                RETURN m + n;
        END;
$$ LANGUAGE plpgsql;

plpgsql ist eine Erweiterung, die in den dockern automatisch aktiviert ist. Eine weitere Extension, die interessant ist, ist plsh - eine sogenannte unsichere Erweiterung. Im Produktivbetrieb sicher mit Vorsicht zu genießen, aber für eine schnelle Kommunikation mit dem umgebenden Host eine unschätzbare kleine Hilfe.

create extension plsh; -- einmalig aktivieren
CREATE OR REPLACE FUNCTION dolog(text) RETURNS text AS $$
#!/bin/bash
echo "$1" >>/tmp/postgres.log
$$ LANGUAGE plsh;

select dolog('moin');
      

Damit lassen sich recht interessante Trigger bauen:

CREATE OR REPLACE FUNCTION dolog() RETURNS trigger AS $$
#!/bin/bash
echo "$1:$2" >> /tmp/postgresql.log
$$ LANGUAGE plsh;

CREATE OR REPLACE TRIGGER onAfterInsertOnDemoLog AFTER INSERT ON demo
    FOR EACH ROW EXECUTE PROCEDURE dolog();

insert into demo values('Moin');
      

Ein solches Log ergibt zunächst wenig Sinn - dafür gibt es Werkzeuge in PostgreSQL. Wofür es aber nützlich ist, ist beispielsweise einen Webhook einzubauen, der einen lokalen curl-Request absetzt, wann immer ein Eintrag in einer Tabelle (z.B. User) sich ändert. Das ginge sicher auch mit einem Polling-Ansatz, aber ich finde diesen Ansatz reizvoll. Eine andere Variante wäre, das listen/notify-Konzept von PostgreSQL zu nutzen.

CREATE OR REPLACE FUNCTION notify_insertion()
  RETURNS trigger AS $$
DECLARE
BEGIN
  PERFORM pg_notify('insertedevents',row_to_json(NEW)::text);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE TRIGGER onAfterInsertOnDemoNotify AFTER INSERT ON demo
    FOR EACH ROW EXECUTE PROCEDURE notify_insertion();
      

Ein entsprechender Listener lässt sich dann beispielsweise mit php und dem pg-Treiber - allerdings nur pollend - realisieren:

<?php
require_once 'config.php';

$conn=pg_connect("host=$host port=$port user=$user password=$password dbname=$db");

pg_query($conn, 'LISTEN insertedevents;');

while(true){
  $notify = pg_get_notify($conn,PGSQL_ASSOC);
  if (!$notify) {
    usleep(10000);
  } else {
    print_r($notify);
  }
}
      

Mit dieser Kombination lassen sich ohne Probleme selbst in unserer stark beschränkten Docker-Umgebung ohne deutliche Mehrbelastung 50.000 Nachrichten (also inserts+notify) weitergeben z.B. an eine Redis-Instanz. Das ist effizienter und sicherer als ein externes Skript aufzurufen, allerdings sind dann natürlich mehr Komponenten zu verwalten und zu monitoren.

Dass uns bei einer solchen pollenden Implementierung unwohl ist, ist korrekt. Eine Variante, bei der wir sowohl verlässlich von unserem Trigger per notify benachrichtigt werden, aber blockierend warten können, funktioniert mit PDO - ist aber wirklich schlecht dokumentiert:

<?php
$conn = new PDO("pgsql:host=localhost dbname=mypgdb", 'mypguser', 'einsicherespasswort');
$conn->exec('LISTEN "insertedevents"');

$num=0;
while(True){
  $result=$conn->pgsqlGetNotify(PDO::FETCH_ASSOC,10000);
  if($result){
    $num++;
    echo("".$num." ".$result["message"]." ".$result["pid"]." ".$result["payload"]."\n");
  }
}
      

Eine weitere Möglichkeit, Webhooks zu nutzen, ist das Debian-Paket postgresql-17-http, das in den Dockern bereits installiert ist. Innerhalb der Datenbank muss die Extension allerdings als Admin-User noch aktiviert werden.


sudo -u postgres psql mypgdb
mypgdb=# create extension http;
select urlencode(jsonb_build_object('name','Colin & James'));
select http_get('https://api.ipify.org?format=json');
select content::json->'ip' from http_get('https://api.ipify.org?format=json');
Kleinigkeiten

Zeige eine spezielle Funktion:

select routine_schema, routine_catalog, routine_name, routine_definition 
from information_schema.routines
where routine_name = 'notify_insertion';
  

Information_schema.columns

select table_catalog, table_schema, table_name, column_name, data_type, is_nullable,
  character_maximum_length as "maxlength", 
  character_octet_length as "octetlength", 
  numeric_scale as "scale" 
from information_schema.columns 
where table_name ='demo';

Information_schema.table:

select table_type, table_catalog, table_schema, table_name, commit_action
from information_schema.tables
where table_catalog = 'mypgdb' and table_schema ='public';

Information_schema.constraints:

select constraint_name, table_catalog, table_schema, table_name, constraint_type
from information_schema.table_constraints;

Information_schema.triggers:

select trigger_catalog, trigger_schema, trigger_name, event_manipulation, event_object_table, 
  action_timing, action_orientation, action_statement 
from information_schema.triggers;

pg_stat_activity (Postgresql spezifisch):

select pid as process_id,
       usename as username,
       datname as database_name,
       client_addr as client_address,
       application_name,
       backend_start,
       state,
       state_change
from pg_stat_activity;

Trigger und Procedure (Postgresql spezifisch)

select tgname,proname,prosrc
from pg_trigger, pg_proc
where  pg_proc.oid=pg_trigger.tgfoid;

Informationen (Postgresql spezifisch)

SELECT pg_size_pretty(pg_total_relation_size('demo'));
-- vacuum full analyze;

Erzeuge eine Tabelle mit 1000 Datensätzen:

insert into gen SELECT generate_series(1,1000), 'moin';
-- oder
insert into gen select i, 'moin' from generate_series(1,1000) as i;
Links
Datenschutz / Impressum