Studienbericht ĂŒber die Implementierung der GeschĂ€ftslogik auf Ebene von gespeicherten Funktionen in PostgreSQL

Der Anstoß fĂŒr das Schreiben dieses Essays war ein Artikel „WĂ€hrend der QuarantĂ€ne ist die Last um das FĂŒnffache gestiegen, aber wir waren bereit.“ Wie Lingualeo mit 23 Millionen Nutzern zu PostgreSQL gewechselt ist. Ich fand auch den vor vier Jahren veröffentlichten Artikel interessant — Implementierung von GeschĂ€ftslogik in MySQL.

Es schien interessant zu sein, dass dieselbe Idee — "die GeschĂ€ftslogik in der DB zu implementieren".

Studienbericht ĂŒber die Implementierung der GeschĂ€ftslogik auf Ebene von gespeicherten Funktionen in PostgreSQL

nicht nur mir allein in den Kopf kam.

Außerdem wollte ich fĂŒr die Zukunft einige interessante Entwicklungen festhalten, die wĂ€hrend der Implementierung entstanden sind. Besonders in Anbetracht der Tatsache, dass vor relativ kurzer Zeit eine strategische Entscheidung getroffen wurde, die Architektur zu wechseln und die GeschĂ€ftslogik auf die Backend-Ebene zu verlagern. So werden alles, was erarbeitet wurde, bald niemandem mehr von Nutzen sein und niemanden interessieren.

Die beschriebenen Methoden sind kein Geheimnis und nichts Außergewöhnliches, Know-how, alles nach Klassik und wurde mehrfach umgesetzt (ich selbst habe einen Ă€hnlichen Ansatz vor 20 Jahren bei Oracle angewendet). Ich habe einfach alles an einem Ort zusammengefasst. Vielleicht kommt es jemandem zugute. Wie die Praxis gezeigt hat, kommt dieselbe Idee oft unabhĂ€ngig von verschiedenen Menschen. Außerdem ist es nĂŒtzlich, um etwas fĂŒr sich selbst als Erinnerung zu hinterlassen.

NatĂŒrlich ist nichts in dieser Welt perfekt, Fehler und Tippfehler sind leider möglich. Kritik und Anmerkungen sind jederzeit willkommen und erwartet. Und noch ein kleines Detail — spezifische Implementierungsdetails wurden weggelassen. Schließlich wird alles momentan in einem funktionierenden Projekt verwendet. Dieser Artikel ist also eher ein Essay und eine Beschreibung des allgemeinen Konzepts, nicht mehr. Ich hoffe, die Details reichen aus, um ein allgemeines Bild zu verstehen.

Die Grundidee — „teile und herrsche, verstecke und besitze“

Die Idee ist klassisch — ein separates Schema fĂŒr Tabellen, ein separates Schema fĂŒr gespeicherte Funktionen.
Der Kunde hat keinen direkten Zugriff auf die Daten. Alles, was der Kunde tun kann, ist, eine gespeicherte Funktion aufzurufen und die erhaltene Antwort zu verarbeiten.

Rollen

CREATE ROLE store;

CREATE ROLE sys_functions;

CREATE ROLE loc_audit_functions;

CREATE ROLE service_functions;

CREATE ROLE business_functions;

Schemata

Schema zur Speicherung von Tabellen

Zieltabellen, die GeschÀftseinheiten implementieren.

CREATE SCHEMA store AUTHORIZATION store;

Schema der Systemfunktionen

Systemfunktionen, insbesondere zum Protokollieren von TabellenÀnderungen.

CREATE SCHEMA sys_functions AUTHORIZATION sys_functions;

Schema der lokalen Audits

Funktionen und Schemata zur DurchfĂŒhrung eines lokalen Audits von gespeicherten Funktionen und Änderungen an Zieltabellen.

CREATE SCHEMA loc_audit_functions AUTHORIZATION loc_audit_functions;

Schemata fĂŒr Dienstleistungsfunktionen

Funktionen fĂŒr Dienstleistungs- und DML-Funktionen.

CREATE SCHEMA service_functions AUTHORIZATION service_functions;

Schemata fĂŒr GeschĂ€fts-funktionen

Funktionen fĂŒr die endgĂŒltigen GeschĂ€fts-funktionen, die vom Kunden aufgerufen werden.

CREATE SCHEMA business_functions AUTHORIZATION business_functions;

Zugriffsrechte

Rolle — DBA hat vollstĂ€ndigen Zugriff auf alle Schemata (separat von der Rolle DB Owner).

CREATE ROLE dba_role;
GRANT store TO dba_role;
GRANT sys_functions TO dba_role;
GRANT loc_audit_functions TO dba_role;
GRANT service_functions TO dba_role;
GRANT business_functions TO dba_role;

Rolle — USER hat das Recht EXECUTE im Schema business_functions.

CREATE ROLE user_role;

Berechtigungen zwischen Schemata

GRANT
Da alle Funktionen mit dem Attribut SECURITY DEFINER ist die Anweisung erforderlich REVOKE EXECUTE ON ALL FUNCTION
 FROM public;

REVOKE EXECUTE ON ALL FUNCTION IN SCHEMA sys_functions FROM public ; 
REVOKE EXECUTE ON ALL FUNCTION IN SCHEMA loc_audit_functions FROM public ; 
REVOKE EXECUTE ON ALL FUNCTION IN SCHEMA service_functions FROM public ; 
REVOKE EXECUTE ON ALL FUNCTION IN SCHEMA business_functions FROM public ; 

GRANT USAGE ON SCHEMA sys_functions TO dba_role ; 
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA sys_functions TO dba_role ;
GRANT USAGE ON SCHEMA loc_audit_functions TO dba_role ; 
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA loc_audit_functions TO dba_role ;
GRANT USAGE ON SCHEMA service_functions TO dba_role ; 
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA service_functions TO dba_role ;
GRANT USAGE ON SCHEMA business_functions TO dba_role ; 
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA business_functions TO dba_role ;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA business_functions TO user_role ;

GRANT ALL PRIVILEGES ON SCHEMA store TO GROUP business_functions ;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA store TO business_functions ;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA store TO business_functions ;

Das Datenbankschema ist jetzt bereit. Sie können mit der BefĂŒllung der Daten beginnen.

Zieltabelle

Die Erstellung der Tabellen ist trivial. Es gibt keine Besonderheiten, bis auf die Entscheidung, auf die Verwendung von SERIAL zu verzichten und Sequenzen explizit zu generieren. DarĂŒber hinaus sollte die Anweisung

COMMENT ON ...

Kommentare fĂŒr Umgebungen und Clustern des Projekts verwendet wird. Dieses Prinzip bildet die Grundlage fĂŒr ein gutes Objekte, ohne Ausnahmen.

Lokaler Audit

Zur Protokollierung der AusfĂŒhrung gespeicherter Funktionen und Änderungen an Zieltabellen wird eine lokale Audit-Tabelle verwendet, die unter anderem Details zur Kundenverbindung, das Label des aufgerufenen Moduls sowie die tatsĂ€chlichen Werte der Eingabe- und Ausgabewerte in JSON-Format enthĂ€lt.

Systemfunktionen

Dienen zur Protokollierung von Änderungen in den Zieltabellen. Sie bestehen aus Triggerfunktionen.

Vorlage — Systemfunktion

---------------------------------------------------------
-- INSERT
CREATE OR REPLACE FUNCTION sys_functions.table_insert_log ()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM loc_audit_functions.make_log( ' '||'table' , 'insert' , json_build_object('id', NEW.id)  );
  RETURN NULL ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER table_after_insert AFTER INSERT ON storage.table FOR EACH ROW EXECUTE PROCEDURE sys_functions.table_insert_log();

---------------------------------------------------------
-- UPDATE
CREATE OR REPLACE FUNCTION sys_functions.table_update_log ()
RETURNS TRIGGER AS $$
BEGIN
  IF OLD.column != NEW.column
  THEN
    PERFORM loc_audit_functions.make_log( ' '||'table' , 'update' , json_build_object('OLD.column', OLD.column , 'NEW.column', NEW.column )  );
  END IF ;
  RETURN NULL ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER table_after_update AFTER UPDATE ON storage.table FOR EACH ROW EXECUTE PROCEDURE sys_functions.table_update_log ();

---------------------------------------------------------
-- DELETE
CREATE OR REPLACE FUNCTION sys_functions.table_delete_log ()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM loc_audit_functions.make_log( ' '||'table' , 'delete' , json_build_object('id', OLD.id )  );
  RETURN NULL ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER table_after_delete AFTER DELETE ON storage.table FOR EACH ROW EXECUTE PROCEDURE sys_functions.table_delete_log ();

Dienstfunktionen

Sie dienen der DurchfĂŒhrung von Service- und DML-Operationen an den Zieltabelle.

Vorlage — Dienstfunktion

--INSERT
--GIBT ID DER NEUEN ZEILE ZURÜCK
CREATE OR REPLACE FUNCTION service_functions.table_insert ( new_column store.table.column%TYPE )
RETURNS integer AS $$
DECLARE
  new_id integer ;
BEGIN
  -- Generiere neue ID
  new_id = nextval('store.table.seq');

  -- FĂŒge in die Tabelle ein
  INSERT INTO store.table
  ( 
    id ,
    column
   )
  VALUES
  (
   new_id ,
   new_column
   );

RETURN new_id ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

--DELETE
--GIBT ANZAHL DER GELÖSCHTEN ZEILEN ZURÜCK
CREATE OR REPLACE FUNCTION service_functions.table_delete ( current_id integer ) 
RETURNS integer AS $$
DECLARE
  rows_count integer  ;    
BEGIN
  DELETE FROM store.table WHERE id = current_id; 

  GET DIAGNOSTICS rows_count = ROW_COUNT;                                                                           

  RETURN rows_count ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;
 
-- DETAILS AKTUALISIEREN
-- GIBT ANZAHL DER AKTUALISIERTEN ZEILEN ZURÜCK
CREATE OR REPLACE FUNCTION service_functions.table_update_column 
(
  current_id integer 
  ,new_column store.table.column%TYPE
) 
RETURNS integer AS $$
DECLARE
  rows_count integer  ; 
BEGIN
  UPDATE  store.table
  SET
    column = new_column
  WHERE id = current_id;

  GET DIAGNOSTICS rows_count = ROW_COUNT;                                                                           

  RETURN rows_count ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

GeschÀftsfunktionen

Sie sind fĂŒr die endgĂŒltigen GeschĂ€ftsfunktionen vorgesehen, die vom Kunden aufgerufen werden. Sie geben immer zurĂŒck — JSON. Um Fehler bei der AusfĂŒhrung abzufangen und zu protokollieren, wird ein Block verwendet, EXCEPTION.

Vorlage — GeschĂ€fts Funktion

CREATE OR REPLACE FUNCTION business_functions.business_function_template(
--Eingabeparameter        
 )
RETURNS JSON AS $$
DECLARE
  ------------------------
  --fĂŒr Ausnahmebehandlung
  error_message text ;
  error_json json ;
  result json ;
  ------------------------ 
BEGIN
--PROTOKOLLIERUNG
  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'STARTED',
    json_build_object
    (
	--Eingabeparameter
    ) 
   );

  PERFORM business_functions.notice('business_function_template');            

  --START BUSINESS TEIL
  --ENDE BUSINESS TEIL

  -- ERFOLGREICHES ERGEBNIS
  PERFORM business_functions.notice('result');
  PERFORM business_functions.notice(result);

  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'FINISHED', 
    json_build_object( 'result',result )
  );

  RETURN result ;
----------------------------------------------------------------------------------------------------------
-- AUSNAHMEN BEHANDELN
EXCEPTION                        
  WHEN OTHERS THEN    
    PERFORM loc_audit_functions.make_log
    (
      'business_function_template',
      'STARTED',
      json_build_object
      (
	--Eingabeparameter	
      ) , TRUE );

     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERROR',
       json_build_object('SQLSTATE',SQLSTATE ), TRUE 
     );

     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERROR',
       json_build_object('SQLERRM',SQLERRM  ), TRUE 
      );

     GET STACKED DIAGNOSTICS error_message = RETURNED_SQLSTATE ;
     PERFORM loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERROR-RETURNED_SQLSTATE',json_build_object('RETURNED_SQLSTATE',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = COLUMN_NAME ;
     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERROR-COLUMN_NAME',
       json_build_object('COLUMN_NAME',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = CONSTRAINT_NAME ;
     PERFORM loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERROR-CONSTRAINT_NAME',
      json_build_object('CONSTRAINT_NAME',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = PG_DATATYPE_NAME ;
     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERROR-PG_DATATYPE_NAME',
       json_build_object('PG_DATATYPE_NAME',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = MESSAGE_TEXT ;
     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERROR-MESSAGE_TEXT',json_build_object('MESSAGE_TEXT',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = SCHEMA_NAME ;
     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERROR-SCHEMA_NAME',json_build_object('SCHEMA_NAME',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = PG_EXCEPTION_DETAIL ;
     PERFORM loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERROR-PG_EXCEPTION_DETAIL',
      json_build_object('PG_EXCEPTION_DETAIL',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = PG_EXCEPTION_HINT ;
     PERFORM loc_audit_functions.make_log
     (
       'business_function_template',
       ' ERROR-PG_EXCEPTION_HINT',json_build_object('PG_EXCEPTION_HINT',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = PG_EXCEPTION_CONTEXT ;
     PERFORM loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERROR-PG_EXCEPTION_CONTEXT',json_build_object('PG_EXCEPTION_CONTEXT',error_message  ), TRUE );                                      

    RAISE WARNING 'ALARM: %' , SQLERRM ;

    SELECT json_build_object
    (
      'isError' , TRUE ,
      'errorMsg' , SQLERRM
     ) INTO error_json ;

  RETURN  error_json ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

Fazit

Um das Gesamtbild zu beschreiben, denke ich, dass das vollkommen ausreicht. Wenn jemand an Details und Ergebnissen interessiert ist – schreibt Kommentare, ich ergĂ€nze das Bild gerne mit zusĂ€tzlichen Feinheiten.

P.S.

Protokollierung eines einfachen Fehlers – Typ des Eingabeparameters

-[ RECORD 1 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1072
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | STARTED
jsonb_pretty    | {
                |     "dko": {
                |         "id": 4,
                |         "type": "Type1",
                |         "title": "ERSTELLT VON addKD",
                |         "Weight": 10,
                |         "Tr": "300",
                |         "reduction": 10,
                |         "isTrud": "TRUE",
                |         "description": "beschreibung",
                |         "lowerTr": "100",
                |         "measurement": "measurement1",
                |         "methodology": "m1",
                |         "passportUrl": "files",
                |         "upperTr": "200",
                |         "weightingFactor": 100.123,
                |         "actualTrValue": null,
                |         "upperTrCalcNumber": "120"
                |     },
                |     "CardId": 3
                | }
-[ RECORD 2 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1073
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER
jsonb_pretty    | {
                |     "SQLSTATE": "22P02"
                | }
-[ RECORD 3 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1074
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER
jsonb_pretty    | {
                |     "SQLERRM": "ungĂŒltige Eingabesyntax fĂŒr Typ numerisch: "null""
                | }
-[ RECORD 4 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1075
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-RETURNED_SQLSTATE
jsonb_pretty    | {
                |     "RETURNED_SQLSTATE": "22P02"
                | }
-[ RECORD 5 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1076
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-SPALTENNAME
jsonb_pretty    | {
                |     "COLUMN_NAME": ""
                | }

-[ RECORD 6 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1077
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-BEDINGUNGNAME
jsonb_pretty    | {
                |     "CONSTRAINT_NAME": ""
                | }
-[ RECORD 7 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1078
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-PG_DATENTYP_NAME
jsonb_pretty    | {
                |     "PG_DATATYPE_NAME": ""
                | }
-[ RECORD 8 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1079
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-MELDUNGSTEXT
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "ungĂŒltige Eingabesyntax fĂŒr Typ numerisch: "null""
                | }
-[ RECORD 9 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1080
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-SCHEMANAME
jsonb_pretty    | {
                |     "SCHEMA_NAME": ""
                | }
-[ RECORD 10 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1081
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-PG_AUSNAHME_DETAIL
jsonb_pretty    | {
                |     "PG_EXCEPTION_DETAIL": ""
                | }
-[ RECORD 11 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1082
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-PG_AUSNAHME_HINWEIS
jsonb_pretty    | {
                |     "PG_EXCEPTION_HINT": ""
                | }
-[ RECORD 12 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1083
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-PG_AUSNAHME_KONTEXT
jsonb_pretty    | {
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  FEHLER-MELDUNGSTEXT
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "ungĂŒltige Eingabesyntax fĂŒr Typ numerisch: "null""
                | }

Quelle: habr.com

60GB SSD 8Gb DDR4