Fehlerbehandlung in PostgreSQL

PostgreSQLBeginner
Jetzt üben

Einführung

In diesem Lab erkunden Sie Techniken zur Fehlerbehandlung in PostgreSQL mit dessen prozeduraler Sprache PL/pgSQL. Sie lernen, wie Sie Funktionen erstellen, die potenzielle Fehler elegant behandeln können, anstatt abzustürzen. Das Lab behandelt das Auslösen von Informationsmeldungen, das Abfangen spezifischer Ausnahmen wie "Division durch Null" und das Protokollieren von Fehlerdetails in einer Datenbanktabelle zur späteren Überprüfung. Am Ende werden Sie verstehen, wie Sie robustere und zuverlässigere Datenbankfunktionen erstellen.

Ausgeben von Hinweisen und Warnungen

In diesem Schritt lernen Sie, die RAISE-Anweisung in einer PL/pgSQL-Funktion zu verwenden. Die RAISE-Anweisung ist nützlich, um Informationsmeldungen, Warnungen oder Debugging-Ausgaben bereitzustellen, ohne die Ausführung der Funktion zu unterbrechen.

Verbinden Sie sich zunächst mit dem interaktiven Terminal von PostgreSQL, psql, als Benutzer postgres. Alle nachfolgenden SQL-Befehle in diesem Lab werden innerhalb dieses Terminals ausgeführt.

sudo -u postgres psql

Sie sehen nun die psql-Eingabeaufforderung, die wie postgres=# aussieht.

Lassen Sie uns eine Funktion namens greet erstellen, die einen Namen als Eingabe nimmt und einen NOTICE auslöst. Notices sind Meldungen mit niedriger Priorität.

Führen Sie den folgenden SQL-Befehl in der psql-Shell aus:

CREATE OR REPLACE FUNCTION greet(name TEXT)
RETURNS TEXT AS $$
BEGIN
  RAISE NOTICE 'Greeting function called with name: %', name;
  RETURN 'Hello, ' || name;
END;
$$ LANGUAGE plpgsql;

Diese Funktion nimmt ein name-Argument entgegen. Die Zeile RAISE NOTICE '...', name; erzeugt eine Notice-Meldung. Das % ist ein Platzhalter, der durch den Wert der Variablen name ersetzt wird.

Rufen Sie nun die Funktion auf, um sie in Aktion zu sehen:

SELECT greet('LabEx');

Die Ausgabe zeigt sowohl die Notice-Meldung als auch den Rückgabewert der Funktion:

NOTICE:  Greeting function called with name: LabEx
   greet
-----------
 Hello, LabEx
(1 row)

Sie können auch Meldungen mit einer höheren Schweregradstufe auslösen, wie z. B. WARNING. Ändern wir die Funktion so, dass sie eine Warnung auslöst, wenn der eingegebene Name leer ist.

CREATE OR REPLACE FUNCTION greet(name TEXT)
RETURNS TEXT AS $$
BEGIN
  IF name IS NULL OR name = '' THEN
    RAISE WARNING 'Input name is empty or NULL.';
    RETURN 'Hello, stranger';
  ELSE
    RAISE NOTICE 'Greeting function called with name: %', name;
    RETURN 'Hello, ' || name;
  END IF;
END;
$$ LANGUAGE plpgsql;

Rufen Sie die Funktion nun mit einem leeren String auf, um die Warnung auszulösen:

SELECT greet('');

Dieses Mal sehen Sie eine WARNING-Meldung anstelle einer NOTICE:

WARNING:  Input name is empty or NULL.
      greet
------------------
 Hello, stranger
(1 row)

Sie haben erfolgreich RAISE verwendet, um verschiedene Arten von Meldungen innerhalb einer Funktion zu generieren.

Auslösen und Abfangen einer Ausnahme

In diesem Schritt lernen Sie, Laufzeitfehler, bekannt als Ausnahmen (exceptions), zu behandeln. Eine unbehandelte Ausnahme beendet Ihre Funktion sofort. Sie erstellen zuerst eine Funktion, die fehlschlägt, und ändern sie dann, um die Ausnahme elegant abzufangen.

Lassen Sie uns eine Funktion simple_divide erstellen, die eine Ganzzahldivision durchführt. Diese Funktion hat einen potenziellen Fehler: Sie stürzt ab, wenn Sie versuchen, durch Null zu teilen.

CREATE OR REPLACE FUNCTION simple_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
END;
$$ LANGUAGE plpgsql;

Rufen Sie zuerst die Funktion mit gültigen Eingaben auf, um zu bestätigen, dass sie korrekt funktioniert:

SELECT simple_divide(10, 2);

Die Ausgabe ist das Ergebnis der Division:

 simple_divide
---------------
             5
(1 row)

Rufen Sie nun die Funktion mit einem Nenner von Null auf, um eine Ausnahme auszulösen:

SELECT simple_divide(10, 0);

Dieser Befehl schlägt fehl und gibt eine Fehlermeldung zurück. Die Ausführung der Funktion wird abgebrochen.

ERROR:  division by zero
CONTEXT:  PL/pgSQL function simple_divide(integer,integer) line 3 at RETURN

Dies ist eine unbehandelte Ausnahme. Um zu verhindern, dass die Funktion abstürzt, können Sie einen EXCEPTION-Block verwenden. Lassen Sie uns eine neue Funktion safe_divide erstellen, die eine Fehlerbehandlung enthält.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    RAISE NOTICE 'Error: Cannot divide by zero.';
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

So funktioniert der EXCEPTION-Block:

  • BEGIN...END;: Dies definiert einen Codeblock.
  • EXCEPTION: Dieses Schlüsselwort startet den Abschnitt zur Ausnahmebehandlung.
  • WHEN division_by_zero THEN: Dies gibt an, dass der folgende Code nur ausgeführt werden soll, wenn ein division_by_zero-Fehler auftritt.
  • Der Code innerhalb des Handlers löst eine Benachrichtigung aus und gibt NULL zurück, anstatt abzustürzen.

Rufen Sie nun die neue Funktion safe_divide mit einem Nenner von Null auf:

SELECT safe_divide(10, 0);

Dieses Mal stürzt die Funktion nicht ab. Sie fängt die Ausnahme ab, zeigt Ihre benutzerdefinierte Benachrichtigung an und gibt NULL zurück.

NOTICE:  Error: Cannot divide by zero.
 safe_divide
-------------

(1 row)

Sie haben erfolgreich eine spezifische Ausnahme abgefangen und die Ausgabe der Funktion gesteuert.

Fehler in eine Tabelle protokollieren

Das Abfangen von Ausnahmen ist gut, aber für die Überwachung und Fehlersuche ist es oft besser, Fehler an einem persistenten Ort zu protokollieren, z. B. in einer Tabelle. In diesem Schritt ändern Sie die Funktion safe_divide, um Fehlerdetails in die Tabelle error_log zu schreiben, die während der Laboreinrichtung erstellt wurde.

Untersuchen Sie zunächst die Struktur der Tabelle error_log mit dem Befehl \d in psql.

\d error_log

Sie sehen die Spalten der Tabelle, darunter eine automatisch inkrementierende id, einen timestamp, die Fehlermeldung (message) und den Funktionsnamen (function_name).

                                              Table "public.error_log"
    Column     |            Type             | Collation | Nullable |                     Default
---------------+-----------------------------+-----------+----------+----------------------------------------------------
 id            | integer                     |           | not null | nextval('error_log_id_seq'::regclass)
 timestamp     | timestamp without time zone |           |          | (now() AT TIME ZONE 'utc'::text)
 message       | text                        |           |          |
 function_name | text                        |           |          |
Indexes:
    "error_log_pkey" PRIMARY KEY, btree (id)

Ändern wir nun die Funktion safe_divide. Im EXCEPTION-Block fügen Sie eine INSERT-Anweisung hinzu, um den Fehler zu protokollieren, bevor Sie zurückkehren.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('Division by zero occurred!', 'safe_divide');

    RAISE NOTICE 'Error: Cannot divide by zero. Details logged.';
    RETURN NULL;
END;
$$ LANGUAGE plpgsql;

Rufen Sie die Funktion nun erneut auf, um den Fehler und die neue Protokollierungslogik auszulösen:

SELECT safe_divide(10, 0);

Die Funktion gibt wie zuvor NULL zurück, hat aber auch eine Datenbankaktualisierung durchgeführt. Um dies zu überprüfen, fragen Sie die Tabelle error_log ab, um den neuen Eintrag zu sehen.

SELECT function_name, message FROM error_log;

Die Ausgabe sollte den protokollierten Fehler anzeigen:

 function_name |          message
---------------+----------------------------
 safe_divide   | Division by zero occurred!
(1 row)

Dies bestätigt, dass Ihre Fehlerbehandlungslogik nun in der Lage ist, Fehlerinformationen für die spätere Analyse zu speichern.

Allgemeine Ausnahmen mit OTHERS behandeln

Bisher haben Sie einen sehr spezifischen Fehler behandelt: division_by_zero. Aber was ist mit anderen, unerwarteten Fehlern? PL/pgSQL bietet einen allgemeinen OTHERS-Ausnahmebehandler, um jeden Fehler abzufangen, der nicht von einer spezifischeren WHEN-Klausel erfasst wurde.

In diesem Schritt erweitern Sie die Funktion safe_divide, um alle möglichen Ausnahmen zu behandeln. Sie verwenden außerdem zwei spezielle Variablen, SQLSTATE (den SQL-Fehlercode) und SQLERRM (die SQL-Fehlermeldung), um detailliertere Informationen zu protokollieren.

Ändern wir die Funktion safe_divide ein letztes Mal.

CREATE OR REPLACE FUNCTION safe_divide(numerator INTEGER, denominator INTEGER)
RETURNS INTEGER AS $$
BEGIN
  RETURN numerator / denominator;
EXCEPTION
  WHEN division_by_zero THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('Division by zero occurred!', 'safe_divide');

    RAISE NOTICE 'Error: Cannot divide by zero. Details logged.';
    RETURN NULL;

  WHEN OTHERS THEN
    INSERT INTO error_log (message, function_name)
    VALUES ('An unexpected error occurred: ' || SQLSTATE || ' - ' || SQLERRM, 'safe_divide');

    RAISE NOTICE 'An unexpected error occurred. Details logged.';
    RETURN -1;
END;
$$ LANGUAGE plpgsql;

Der neue WHEN OTHERS-Block fängt jeden anderen Fehler ab. Er erstellt eine detaillierte Nachricht unter Verwendung der Variablen SQLSTATE und SQLERRM und fügt sie in das Protokoll ein. Anschließend gibt er -1 zurück, um anzuzeigen, dass ein allgemeiner Fehler aufgetreten ist.

Obwohl unsere aktuelle Funktion nur einen division_by_zero-Fehler auslösen kann, macht diese Struktur sie wesentlich robuster gegenüber zukünftigen Änderungen oder unerwarteten Eingaben. Wenn beispielsweise die Eingabetypen in einen Typ geändert würden, der einen Überlauf verursachen könnte, würde der OTHERS-Block den Fehler numeric_value_out_of_range abfangen.

Testen wir den division_by_zero-Pfad erneut, um sicherzustellen, dass er weiterhin wie erwartet funktioniert. Löschen Sie zuerst die Protokolltabelle für einen sauberen Test.

TRUNCATE error_log;

Rufen Sie nun die Funktion auf:

SELECT safe_divide(10, 0);

Überprüfen Sie abschließend das Protokoll. Es sollte die spezifische Meldung "Division by zero" enthalten, was bestätigt, dass der erste WHEN-Block korrekt ausgewählt wurde.

SELECT message FROM error_log;
          message
----------------------------
 Division by zero occurred!
(1 row)

Sie haben nun eine widerstandsfähige Funktion erstellt, die sowohl spezifische, erwartete Fehler als auch allgemeine, unerwartete Fehler behandelt.

Zusammenfassung

In diesem Lab haben Sie die Grundlagen der Fehlerbehandlung in PostgreSQLs PL/pgSQL gelernt. Sie haben mit der RAISE-Anweisung begonnen, um NOTICE- und WARNING-Meldungen zu Debugging- und Informationszwecken auszugeben. Anschließend haben Sie Laufzeitausnahmen behandelt, indem Sie eine Funktion erstellt haben, die fehlschlagen konnte, und dann einen BEGIN...EXCEPTION...END-Block implementiert haben, um einen spezifischen division_by_zero-Fehler abzufangen. Darauf aufbauend haben Sie die Funktion erweitert, um Fehlerdetails in eine Datenbanktabelle zu protokollieren und so einen persistenten Datensatz für die Überwachung zu schaffen. Schließlich haben Sie die Funktion robuster gemacht, indem Sie einen allgemeinen WHEN OTHERS-Handler hinzugefügt haben, um unerwartete Fehler abzufangen und sicherzustellen, dass Ihre Funktion unter verschiedenen Bedingungen ordnungsgemäß fehlschlagen kann.