Studio sull'implementazione della logica di business a livello di funzioni memorizzate in PostgreSQL

L'idea principale per la stesura dell'analisi è stata un articolo «Durante il lockdown il carico è aumentato di 5 volte, ma eravamo pronti». Come Lingualeo è passato a PostgreSQL con 23 milioni di utenti. Sembrava interessante anche un articolo pubblicato 4 anni fa — Implementazione della logica di business in MySQL.

Mi è sembrato interessante che lo stesso pensiero-"implementare la logica di business nel database".

Studio sull'implementazione della logica di business a livello di funzioni memorizzate in PostgreSQL

non sia venuto solo a me.

Inoltre, vorrei conservare, per me innanzitutto, idee interessanti emerse durante l'implementazione. Soprattutto considerando che relativamente recentemente è stata presa una decisione strategica di cambiare architettura e trasferire la logica di business a livello backend. Quindi, tutto ciò che è stato sviluppato presto non servirà più a nessuno e non interesserà più a nessuno.

I metodi descritti non rappresentano una scoperta o qualcosa di eccezionale know how, tutto secondo la tradizione e già implementato più volte (ad esempio, io ho applicato un approccio simile 20 anni fa su Oracle). Ho semplicemente deciso di raccogliere tutto in un unico posto. Chissà, potrebbe servire a qualcuno. Come ha dimostrato la pratica — piuttosto spesso la stessa idea viene in mente a persone diverse indipendentemente. E tenerla a mente è utile.

Naturalmente, nulla è perfetto in questo mondo, errori e refusi sono purtroppo possibili. Critiche e commenti sono sempre benvenuti e attesi. E un altro piccolo dettaglio — i dettagli specifici dell'implementazione sono stati omessi. Del resto, tutto è in uso in un progetto attualmente funzionante. Quindi, l'articolo è un'analisi e una descrizione di un concetto generale, non di più. Spero che per capire il quadro generale, i dettagli siano sufficienti.

L'idea generale è — «dividi e conquista, nascondi e possiedi»

L'idea è classica — uno schema separato per le tabelle, uno schema separato per le funzioni memorizzate.
Il cliente non ha accesso diretto ai dati. Tutto ciò che il cliente può fare è solo chiamare una funzione memorizzata e elaborare la risposta ricevuta.

Ruoli

CREATE ROLE store;

CREATE ROLE sys_functions;

CREATE ROLE loc_audit_functions;

CREATE ROLE service_functions;

CREATE ROLE business_functions;

Schemi

Schema di archiviazione delle tabelle

Tabelle di destinazione che implementano entità tematiche.

CREATE SCHEMA store AUTHORIZATION store ;

Schema delle funzioni di sistema

Funzioni di sistema, in particolare per il logging delle modifiche delle tabelle.

CREATE SCHEMA sys_functions AUTHORIZATION sys_functions ;

Schema di audit locale

Funzioni e tabelle per l'audit locale dell'esecuzione di funzioni memorizzate e della modifica delle tabelle di destinazione.

CREATE SCHEMA loc_audit_functions AUTHORIZATION loc_audit_functions;

Schema delle funzioni di servizio

Funzioni per funzioni di servizio e DML.

CREATE SCHEMA service_functions AUTHORIZATION service_functions;

Schema delle funzioni di business

Funzioni per le funzioni di business finali invocate dal cliente.

CREATE SCHEMA business_functions AUTHORIZATION business_functions;

Diritti di accesso

Ruolo — DBA ha accesso completo a tutti gli schemi (distinto dal ruolo di 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;

Ruolo — UTENTE ha il privilegio EXECUTE nello schema business_functions.

CREATE ROLE user_role;

Privilegi tra schemi

GRANT
Poiché tutte le funzioni vengono create con l'attributo SECURITY DEFINER è necessaria l'istruzione 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 ;

Quindi, lo schema del DB è pronto. Si può iniziare a popolare con i dati.

Tabelle di destinazione

La creazione delle tabelle è banale. Nessuna caratteristica particolare, tranne per il fatto che è stato deciso di rinunciare all'uso di SERIAL e generare sequenze esplicitamente. Inoltre, ovviamente, utilizzo massimo dell'istruzione

COMMENT ON ...

Commenti per tutti oggetti, senza eccezioni.

Audit locale

Per tenere un registro dell'esecuzione di funzioni memorizzate e della modifica delle tabelle di destinazione, viene utilizzata una tabella di audit locale, che include anche i dettagli della connessione del cliente, l'etichetta del modulo invocato e i valori effettivi dei parametri di input e output in formato JSON.

Funzioni di sistema

Destinate alla registrazione delle modifiche nelle tabelle di destinazione. Si tratta di funzioni di attivazione.

Modello — funzione di sistema

---------------------------------------------------------
-- INSERISCI
CREATE OR REPLACE FUNCTION sys_functions.table_insert_log ()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM loc_audit_functions.make_log( ' '||'table' , 'inserire' , 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();

---------------------------------------------------------
-- AGGIORNA
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' , 'aggiornare' , 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 ();

---------------------------------------------------------
-- ELIMINA
CREATE OR REPLACE FUNCTION sys_functions.table_delete_log ()
RETURNS TRIGGER AS $$
BEGIN
  PERFORM loc_audit_functions.make_log( ' '||'table' , 'eliminare' , 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 ();

Funzioni di servizio

Destinato all'implementazione di operazioni di servizio e DML sulle tabelle di destinazione.

Modello - funzione di servizio

--INSERISCI
--RESTITUISCI l'id DEL NUOVO RIGA
CREATE OR REPLACE FUNCTION service_functions.table_insert ( new_column store.table.column%TYPE )
RETURNS integer AS $$
DECLARE
  new_id integer ;
BEGIN
  -- Genera nuovo id
  new_id = nextval('store.table.seq');

  -- Inserisci nella tabella
  INSERT INTO store.table
  ( 
    id ,
    column
   )
  VALUES
  (
   new_id ,
   new_column
   );

RETURN new_id ;
END
$$ LANGUAGE plpgsql SECURITY DEFINER;

--ELIMINA
--RESTITUISCI IL NUMERO DELLE RIGHE ELIMINATE
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;
 
-- DETTAGLI AGGIORNAMENTO
-- RESTITUISCI IL NUMERO DELLE RIGHE AGGIORNATE
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;

Funzioni di business

Destinato alle funzioni aziendali finali chiamate dal cliente. Restituiscono sempre — JSON. Per intercettare e registrare gli errori di esecuzione, si utilizza il blocco ECCEZIONE.

Modello - funzione di business

CREA O SOSTITUISCI LA FUNZIONE business_functions.business_function_template(
--Parametri di input        
 )
RESTITUISCE JSON AS $$
DICHIARARE
  ------------------------
  --per la gestione delle eccezioni
  error_message text ;
  error_json json ;
  result json ;
  ------------------------ 
INIZIO
--LOGGING
  PERFORM loc_audit_functions.make_log
  (
    'business_function_template',
    'AVVIATO',
    json_build_object
    (
	--Parametri di input
    ) 
   );

  PERFORM business_functions.notice('business_function_template');            

  --INIZIO PARTE AZIENDALE
  --FINE PARTE AZIENDALE

  -- RISULTATO SUCCESSOSO
  PERFORM business_functions.notice('result');
  PERFORM business_functions.notice(result);

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

  RESTITUISCI result ;
----------------------------------------------------------------------------------------------------------
-- GESTIONE DELLE ECCEZIONI
ECCEZIONE                        
  QUANDO ALTRO ALLORA    
    PERFORM loc_audit_functions.make_log
    (
      'business_function_template',
      'AVVIATO',
      json_build_object
      (
	--Parametri di input	
      ) , TRUE );

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

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

     GET STACKED DIAGNOSTICS error_message = RETURNED_SQLSTATE ;
     PERFORM loc_audit_functions.make_log
     (
      'business_function_template',
      ' ERRORE-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',
       ' ERRORE-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',
      ' ERRORE-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',
       ' ERRORE-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',
       ' ERRORE-MESSAGE_TEXT',json_build_object('MESSAGE_TEXT',error_message  ), TRUE );

     GET STACKED DIAGNOSTICS error_message = SCHEMA_NAME ;
     PERFORM loc_audit_functions.make_log
     (s
       'business_function_template',
       ' ERRORE-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',
      ' ERRORE-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',
       ' ERRORE-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',
      ' ERRORE-PG_EXCEPTION_CONTEXT',json_build_object('PG_EXCEPTION_CONTEXT',error_message  ), TRUE );                                      

    ALZA AVVISO 'ALLARME: %' , SQLERRM ;

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

  RESTITUISCI  error_json ;
FINE
$$ LINGUAGGIO plpgsql SECURITY DEFINER;

Risultato

Per descrivere il quadro generale, penso sia più che sufficiente. Se qualcuno è interessato ai dettagli e ai risultati, scrivete nei commenti, sarò lieto di arricchire il quadro con ulteriori dettagli.

P.S.

Il logging di un errore semplice — tipo di parametro di input

-[ RECORD 1 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1072
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          | INIZIATO
jsonb_pretty    | {
                |     "dko": {
                |         "id": 4,
                |         "type": "Tipo1",
                |         "title": "CREATO DA addKD",
                |         "Weight": 10,
                |         "Tr": "300",
                |         "reduction": 10,
                |         "isTrud": "VERO",
                |         "description": "descrizione",
                |         "lowerTr": "100",
                |         "measurement": "misura1",
                |         "methodology": "m1",
                |         "passportUrl": "file",
                |         "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          |  ERRORE
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          |  ERRORE
jsonb_pretty    | {
                |     "SQLERRM": "sintassi di input non valida per tipo numerico: "null""
                | }
-[ RECORD 4 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1075
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  ERRORE-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          |  ERRORE-NOME_COLONNA
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          |  ERRORE-NOME vincolato
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          |  ERRORE-NOME_TIPO_DATI_PG
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          |  ERRORE-TEXT_MESSAGE
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "sintassi di input non valida per tipo numerico: "null""
                | }
-[ RECORD 9 ]-
date_trunc      | 2020-08-19 13:15:46
id              | 1080
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  ERRORE-NOME_SCHEMA
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          |  ERRORE-DETTAGLIO ECCEZIONE PG
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          |  ERRORE-SUGGERIMENTO ECCEZIONE PG
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          |  ERRORE-CONTEXT ECCEZIONE PG
jsonb_pretty    | {
usename         | emp1
log_module      | addKD
log_module_hash | 0b4c1529a89af3ddf6af3821dc790e8a
status          |  ERRORE-TEXT_MESSAGE
jsonb_pretty    | {
                |     "MESSAGE_TEXT": "sintassi di input non valida per tipo numerico: "null""
                | }

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster